From 9f8b98df264046c2061c287385892b06ebe1fbe2 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Thu, 11 Aug 2016 23:29:05 +0000 Subject: [PATCH 01/86] Added package.json for plugin_router. --- plugin_router/package.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 plugin_router/package.json diff --git a/plugin_router/package.json b/plugin_router/package.json new file mode 100644 index 000000000..a01f69dde --- /dev/null +++ b/plugin_router/package.json @@ -0,0 +1,31 @@ +{ + "name": "@horizon/plugin_router", + "version": "1.0.0", + "description": "Plugin router for Horizon.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "coverage": "istanbul cover _mocha test/test.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme" + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + } +} From 98e828a4f05d7079814a27d3a93f32ff6795663d Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Fri, 12 Aug 2016 00:59:12 +0000 Subject: [PATCH 02/86] Added index.js to plugin_router. --- plugin_router/package.json | 1 - plugin_router/src/index.js | 77 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 plugin_router/src/index.js diff --git a/plugin_router/package.json b/plugin_router/package.json index a01f69dde..720bcceb8 100644 --- a/plugin_router/package.json +++ b/plugin_router/package.json @@ -6,7 +6,6 @@ "scripts": { "lint": "eslint src test", "test": "mocha test", - "coverage": "istanbul cover _mocha test/test.js" }, "repository": { "type": "git", diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js new file mode 100644 index 000000000..6e3ab8f45 --- /dev/null +++ b/plugin_router/src/index.js @@ -0,0 +1,77 @@ +'use strict'; + +const examplePluginActivateResult = { + name: 'graphql', + deactivate: () => { }, + + httpRoute: (req, res, next) => { }, + commands: { + 'repair': ..., + } + methods: { + 'insert': ..., + 'delete': ..., + }, +} + +class PluginRouter { + constructor() { + this.plugins = {} + this.methods = {} + } + + addPlugin(plugin) { + if (this.plugins[plugin.name]) { + throw new Error(`Plugin conflict: '${plugin.name}' already present.`); + } + for (const m in plugin.methods) { + if (this.methods[m]) { + throw new Error(`Method name conflict: '${m}'); + } + } + + this.plugins[plugin.name] = plugin; + for (const m in plugin.methods) { + this.methods[m] = plugin.methods[m]; + } + } + + removePlugin(plugin) { + if (!this.plugins[plugin.name]) { + throw new Error(`Plugin '${plugin.name}' is not present.`); + } + delete this.plugins[plugin.name]; + } + + httpMiddleware() { + return (req, res, next) => { + const pathParts = req.path.split('/'); + const name = pathParts[0] || pathParts[1]; + const plugin = this.plugins[name]; + if (plugin && plugin.httpRoute) { + plugin.httpRouter(req, res, next); + } else { + next(); + } + } + } + + hzMiddleware() { + return (req, res, next) { + const method = req.type && this.methods[req.type]; + if (method) { + method(req, res, next); + } else { + next(); + } + } + } +} + +function createPluginRouter() { + return new PluginRouter(); +} + +module.exports = createPluginRouter(); +module.exports.PluginRouter = PluginRouter; + From 8b3bcac42af13be99b47cded3d0cb34041ac0223 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Fri, 12 Aug 2016 04:59:47 +0000 Subject: [PATCH 03/86] Added package.json for permissions plugin. --- plugins/permissions/package.json | 30 +++++++++++++++++++ ...actory_factory_pattern_strategy_factory.js | 0 server/src/{request.js => response.js} | 0 3 files changed, 30 insertions(+) create mode 100644 plugins/permissions/package.json create mode 100644 server/src/middleware_pattern_factory_factory_pattern_strategy_factory.js rename server/src/{request.js => response.js} (100%) diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json new file mode 100644 index 000000000..f7cd42fea --- /dev/null +++ b/plugins/permissions/package.json @@ -0,0 +1,30 @@ +{ + "name": "@horizon/plugins/permissions", + "version": "1.0.0", + "description": "Default permissions plugin for Horizon.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme" + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + } +} diff --git a/server/src/middleware_pattern_factory_factory_pattern_strategy_factory.js b/server/src/middleware_pattern_factory_factory_pattern_strategy_factory.js new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/request.js b/server/src/response.js similarity index 100% rename from server/src/request.js rename to server/src/response.js From e1e13c69c33498ee93253d06c985255bff0f91e0 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Fri, 12 Aug 2016 05:36:01 +0000 Subject: [PATCH 04/86] checkpoint --- examples/health-check-plugin/health-check.js | 35 ++++++ plugin_router/src/index.js | 74 ++++++++---- plugins/permissions/package.json | 2 +- plugins/permissions/src/permissions.js | 22 ++++ server/src/client.js | 11 +- server/src/response.js | 41 +++++++ server/src/server.js | 114 +------------------ 7 files changed, 166 insertions(+), 133 deletions(-) create mode 100644 examples/health-check-plugin/health-check.js create mode 100644 plugins/permissions/src/permissions.js diff --git a/examples/health-check-plugin/health-check.js b/examples/health-check-plugin/health-check.js new file mode 100644 index 000000000..b8661ce7a --- /dev/null +++ b/examples/health-check-plugin/health-check.js @@ -0,0 +1,35 @@ +'use strict'; + +// RSI: check connection +module.exports = (config) => { + return { + name: 'health-check', + activate: (ctx) => { + ctx.logger.info('Activating health-check module.'); + return { + commands: { + 'health-check': (args) => { + console.log(`Got ${JSON.stringify(args)}.`); + }, + }, + httpRoute: (req, res, next) => { + console.log(`httpRoute: ${[req, res, next]}`) + res.send("healthy"); + }, + methods: { + 'healthCheck': (req, res, next) => { + console.log(`healthCheck method: ${[req, res, next]}`) + res.send("healthy"); + }, + }, + middleware: (req, res, next) => { + req.healthy = true; + next(); + }, + }; + }, + deactivate: (reason) => { + ctx.logger.info(`Deactivating health-check module (${reason}).`) + }, + }; +} diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 6e3ab8f45..8ea1fe55e 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -15,39 +15,55 @@ const examplePluginActivateResult = { } class PluginRouter { - constructor() { + constructor(server) { + this.server = server this.plugins = {} + this.httpRoutes = {} this.methods = {} } - addPlugin(plugin) { + add(plugin) { if (this.plugins[plugin.name]) { - throw new Error(`Plugin conflict: '${plugin.name}' already present.`); + return Promise.reject( + new Error(`Plugin conflict: '${plugin.name}' already present.`)); } - for (const m in plugin.methods) { - if (this.methods[m]) { - throw new Error(`Method name conflict: '${m}'); + const activePlugin = Promise.resolve(server.ctx()).then(plugin.activate) + this.plugins[plugin.name] = activePlugin.then((active) => { + if (this.httpRoutes[plugin.name]) { + throw new Error(`Plugin conflict: '${plugin.name}' already present.`); + } + // RSI: validate method name is a legal identifier and doesn't + // conflict with our methods. + for (const m in active.methods) { + if (this.methods[m]) { + throw new Error(`Method name conflict: '${m}'); + } } - } - this.plugins[plugin.name] = plugin; - for (const m in plugin.methods) { - this.methods[m] = plugin.methods[m]; - } + this.httpRoutes[plugin.name] = active; + for (const m in active.methods) { + this.methods[m] = active.methods[m]; + } + }); + return this.plugins[plugin.name]; } - removePlugin(plugin) { + remove(plugin, reason) { if (!this.plugins[plugin.name]) { - throw new Error(`Plugin '${plugin.name}' is not present.`); + return Promise.reject(new Error(`Plugin '${plugin.name}' is not present.`)); + } + return this.plugins[plugin.name].then(() => { + if (plugin.deactivate) { + plugin.deactivate(reason || "Removed from PluginRouter."); + } } - delete this.plugins[plugin.name]; } httpMiddleware() { return (req, res, next) => { const pathParts = req.path.split('/'); const name = pathParts[0] || pathParts[1]; - const plugin = this.plugins[name]; + const plugin = this.httpRoutes[name]; if (plugin && plugin.httpRoute) { plugin.httpRouter(req, res, next); } else { @@ -58,12 +74,30 @@ class PluginRouter { hzMiddleware() { return (req, res, next) { - const method = req.type && this.methods[req.type]; - if (method) { - method(req, res, next); - } else { - next(); + const method = (req.type && this.methods[req.type]) || next; + let cb = method; + if (req.options) { + for (const o in req.options) { + if (o !== req.type) { + const m = this.methods[o]; + if (m) { + const old_cb = cb; + cb = (maybeErr) => { + if (maybeErr instanceof Error) { + next(maybeErr); + } else { + try { + m(req, res, old_cb); + } catch (e) { + next(e); + } + } + } + } + } + } } + cb(); } } } diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index f7cd42fea..672c7e854 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -2,7 +2,7 @@ "name": "@horizon/plugins/permissions", "version": "1.0.0", "description": "Default permissions plugin for Horizon.", - "main": "src/index.js", + "main": "src/permissions.js", "scripts": { "lint": "eslint src test", "test": "mocha test", diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js new file mode 100644 index 000000000..d32931249 --- /dev/null +++ b/plugins/permissions/src/permissions.js @@ -0,0 +1,22 @@ +'use strict'; + +// RSI: check connection +module.exports = (config) => { + return { + name: 'permissions', + activate: (ctx) => { + + + + ctx.logger.info('Activating permissions.'); + return { + middleware: (req, res, next) => { + const currentUser = req.userFeed + }, + }; + }, + deactivate: (reason) => { + ctx.logger.info(`Deactivating health-check module (${reason}).`) + }, + }; +} diff --git a/server/src/client.js b/server/src/client.js index a6510ec40..70f837860 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -148,11 +148,14 @@ class Client { if (raw_request === undefined) { return; } else if (raw_request.type === 'end_subscription') { - return this.remove_request(raw_request); // there is no response for end_subscription + // there is no response for end_subscription + return this.remove_request(raw_request); } else if (raw_request.type === 'keepalive') { return this.send_response(raw_request, { state: 'complete' }); } + this._server.handle(raw_request); + const endpoint = this._server.get_request_handler(raw_request); if (endpoint === undefined) { return this.send_error(raw_request, @@ -181,7 +184,8 @@ class Client { close(info) { if (this.is_open()) { - const close_msg = (info.error && info.error.substr(0, 64)) || 'Unspecified reason.'; + const close_msg = + (info.error && info.error.substr(0, 64)) || 'Unspecified reason.'; logger.debug('Closing client connection with message: ' + `${info.error || 'Unspecified reason.'}`); logger.debug(`info: ${JSON.stringify(info)}`); @@ -202,7 +206,8 @@ class Client { } send_error(request, err, code) { - logger.debug(`Sending error result for request ${request.request_id}:\n${err.stack}`); + logger.debug( + `Sending error result for request ${request.request_id}:\n${err.stack}`); const error = err instanceof Error ? err.message : err; const error_code = code === undefined ? -1 : code; diff --git a/server/src/response.js b/server/src/response.js index bcbdcc69b..73627adab 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -3,6 +3,47 @@ const logger = require('./logger'); const rule = require('./permissions/rule'); +class Response { + constructor(requestId, socketSend) { + this._requestId = requestId; + this._socketSend = socketSend; + this.complete = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }).then((data) => { + this._completed = true; + write(data, 'complete'); + }).catch((err) => { + this._completed = true; + this._socketSend({ + request_id: this._requestId, + error: `${err}`, + error_code: err.code || -1, + }); + throw err; + }); + } + + write(data, state = undefined) { + if (this._completed && state !== 'complete') { + throw new Error('This response has already completed.'); + } else if (!this._completed && state === 'complete') { + throw new Error( + '`.write()` cannot be used to send a `state: complete` message.' + + ' Use `.end()` to complete a Response.'); + } + this._socketSend({state, data, request_id: this._requestId}); + } + + end(dataOrError) { + if (dataOrError instanceof Error) { + this._reject(dataOrError); + } else { + this._resolve(dataOrError); + } + } +} + class Request { constructor(raw_request, endpoint, client) { this._raw_request = raw_request; diff --git a/server/src/server.js b/server/src/server.js index 2de356f99..626b3b193 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -11,17 +11,6 @@ const getType = require('mime-types').contentType; // library. Minified, Rx included etc. const horizon_client_path = require.resolve('@horizon/client/dist/horizon'); -const endpoints = { - insert: require('./endpoint/insert'), - query: require('./endpoint/query'), - remove: require('./endpoint/remove'), - replace: require('./endpoint/replace'), - store: require('./endpoint/store'), - subscribe: require('./endpoint/subscribe'), - update: require('./endpoint/update'), - upsert: require('./endpoint/upsert'), -}; - const assert = require('assert'); const fs = require('fs'); const Joi = require('joi'); @@ -40,30 +29,6 @@ const accept_protocol = (protocols, cb) => { } }; -const serve_file = (file_path, res) => { - fs.access(file_path, fs.R_OK | fs.F_OK, (exists) => { - if (exists) { - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end(`Client library not found\n`); - } else { - fs.readFile(file_path, 'binary', (err, file) => { - if (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(`${err}\n`); - } else { - const type = getType(path.extname(file_path)) || false; - if (type) { - res.writeHead(200, { 'Content-Type': type }); - } else { - res.writeHead(200); - } - res.end(file, 'binary'); - } - }); - } - }); -}; - class Server { constructor(http_servers, user_opts) { const opts = Joi.attempt(user_opts || { }, options_schema); @@ -72,12 +37,12 @@ class Server { this._permissions_enabled = opts.permissions; this._auth_methods = { }; this._request_handlers = new Map(); - this._http_handlers = new Map(); this._ws_servers = [ ]; this._close_promise = null; this._interruptor = new Promise((resolve, reject) => { this._interrupt = reject; }); + this._middlewares = [ ]; try { this._reql_conn = new ReqlConnection(opts.rdb_host, @@ -115,52 +80,10 @@ class Server { this._ws_servers.push(ws_server); }; - const path_replace = new RegExp('^' + this._path + '/'); - const add_http_listener = (server) => { - // TODO: this doesn't play well with a user removing listeners (or maybe even `once`) - const extant_listeners = server.listeners('request').slice(0); - server.removeAllListeners('request'); - server.on('request', (req, res) => { - const req_path = url.parse(req.url).pathname; - if (req_path.indexOf(`${this._path}/`) === 0) { - const sub_path = req_path.replace(path_replace, ''); - const handler = this._http_handlers.get(sub_path); - if (handler !== undefined) { - logger.debug(`Handling HTTP request to horizon subpath: ${sub_path}`); - return handler(req, res); - } - } - if (extant_listeners.length === 0) { - res.statusCode = 404; - res.write('File not found.'); - res.end(); - } else { - extant_listeners.forEach((l) => l.call(server, req, res)); - } - }); - }; - - this.add_http_handler('horizon.js', (req, res) => { - serve_file(horizon_client_path, res); - }); - - this.add_http_handler('horizon.js.map', (req, res) => { - serve_file(`${horizon_client_path}.map`, res); - }); - - this.add_http_handler('auth_methods', (req, res) => { - res.writeHead(200, { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': opts.access_control_allow_origin, - }); - res.end(JSON.stringify(this._auth_methods)); - }); - if (http_servers.forEach === undefined) { add_websocket(http_servers); - add_http_listener(http_servers); } else { - http_servers.forEach((s) => { add_websocket(s); add_http_listener(s); }); + http_servers.forEach((s) => add_websocket(s)); } } catch (err) { this._interrupt(err); @@ -168,38 +91,11 @@ class Server { } } - add_request_handler(request_name, endpoint) { - assert(endpoint !== undefined); - assert(this._request_handlers.get(request_name) === undefined); - this._request_handlers.set(request_name, endpoint); - } - - get_request_handler(request) { - return this._request_handlers.get(request.type); - } - - remove_request_handler(request_name) { - return this._request_handlers.delete(request_name); + add_middleware(mw) { + this._middlewares.push(mw); } - add_http_handler(sub_path, handler) { - logger.debug(`Added HTTP handler at ${this._path}/${sub_path}`); - assert.notStrictEqual(handler, undefined); - assert.strictEqual(this._http_handlers.get(sub_path), undefined); - this._http_handlers.set(sub_path, handler); - } - - remove_http_handler(sub_path) { - return this._http_handlers.delete(sub_path); - } - - add_auth_provider(provider, options) { - assert(provider.name); - assert(options.path); - assert.strictEqual(this._auth_methods[provider.name], undefined); - this._auth_methods[provider.name] = `${this._path}/${options.path}`; - provider(this, options); - } + ready() { return this._reql_conn.ready().then(() => this); From f123d528672ac52672e771c9875dc4a18885cb31 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Sat, 13 Aug 2016 06:33:24 +0000 Subject: [PATCH 05/86] checkpoint --- server/src/horizon.js | 7 +- server/src/metadata/metadata.js | 9 ++- server/src/server.js | 128 ++++++++++++++++++-------------- 3 files changed, 80 insertions(+), 64 deletions(-) diff --git a/server/src/horizon.js b/server/src/horizon.js index e0b8baec5..e7e4e0b7b 100644 --- a/server/src/horizon.js +++ b/server/src/horizon.js @@ -9,10 +9,11 @@ joi.validate('', joi.any().when('', { is: '', then: joi.any() })); const server = require('./server'); -const create_server = (http_servers, options) => - new server.Server(http_servers, options); +function createServer(httpServers, options) { + return new server.Server(httpServers, options); +} -module.exports = create_server; +module.exports = createServer; module.exports.Server = server.Server; module.exports.r = require('rethinkdb'); diff --git a/server/src/metadata/metadata.js b/server/src/metadata/metadata.js index 468e05c95..76e8ba11b 100644 --- a/server/src/metadata/metadata.js +++ b/server/src/metadata/metadata.js @@ -42,14 +42,15 @@ const initialize_metadata = (db, conn) => ]) ); +// RSI: pick up here, make this reliable. class Metadata { constructor(project_name, - conn, + reliable_conn, clients, auto_create_collection, auto_create_index) { this._db = project_name; - this._conn = conn; + this._reliable_conn = reliable_conn; this._clients = clients; this._auto_create_collection = auto_create_collection; this._auto_create_index = auto_create_index; @@ -63,8 +64,8 @@ class Metadata { this._ready_promise = Promise.resolve().then(() => { logger.debug('checking rethinkdb version'); - return r.db('rethinkdb').table('server_status').nth(0)('process')('version').run(this._conn) - .then((res) => utils.rethinkdb_version_check(res)); + const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version') + return q.run(this._conn).then((res) => utils.rethinkdb_version_check(res)); }).then(() => { const old_metadata_db = `${this._db}_internal`; return r.dbList().contains(old_metadata_db).run(this._conn).then((has_old_db) => { diff --git a/server/src/server.js b/server/src/server.js index 626b3b193..334ff24e2 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -20,7 +20,7 @@ const websocket = require('ws'); const protocol_name = 'rethinkdb-horizon-v0'; -const accept_protocol = (protocols, cb) => { +function handleProtocols(protocols, cb) { if (protocols.findIndex((x) => x === protocol_name) !== -1) { cb(true, protocol_name); } else { @@ -29,92 +29,106 @@ const accept_protocol = (protocols, cb) => { } }; -class Server { +class Server extends EventEmitter { constructor(http_servers, user_opts) { const opts = Joi.attempt(user_opts || { }, options_schema); - this._path = opts.path; - this._name = opts.project_name; - this._permissions_enabled = opts.permissions; + this._original_user_opts = user_opts; this._auth_methods = { }; this._request_handlers = new Map(); this._ws_servers = [ ]; this._close_promise = null; - this._interruptor = new Promise((resolve, reject) => { - this._interrupt = reject; - }); this._middlewares = [ ]; - try { - this._reql_conn = new ReqlConnection(opts.rdb_host, - opts.rdb_port, - opts.project_name, - opts.auto_create_collection, - opts.auto_create_index, - opts.rdb_user || null, - opts.rdb_password || null, - opts.rdb_timeout || null, - this._interruptor); - this._auth = new Auth(this, opts.auth); - for (const key in endpoints) { - this.add_request_handler(key, endpoints[key].run); + this._reliable_conn = new ReliableConn({ + host: opts.rbd_host, + port: opts.rdb_pot, + db: opts.project_name, + user: opts.rdb_user || 'admin', + password: opts.rdb_password || '', + timeout: opts.rdb_timeout || null, + }); + this._clients = new Set(); + + this._reliable_metadata = new ReliableMetadata( + opts.project_name, + this._reliable_conn, + this._clients, + opts.auto_create_collection, + opts.auto_create_index); + this._clear_clients_subscription = this._reliable_metadata.subscribe({ + onReady: () => { + this.emit('ready', this); + } + onUnready: (err) => { + this.emit('unready', this); + const msg = (err && err.message) || 'Connection became unready.'; + this._clients.forEach((client) => client.close({ error: msg })); + this._clients.clear(); + } + }); + + this._auth = new Auth(this, opts.auth); + for (const key in endpoints) { + this.add_request_handler(key, endpoints[key].run); + } + + const verifyClient = (info, cb) => { + // Reject connections if we aren't synced with the database + if (!this._reliable_metadata.isReady()) { + cb(false, 503, 'Connection to the database is down.'); + } else { + cb(true); } + }; + + const ws_options = { handleProtocols, verifyClient, path: opts.path }; - const verify_client = (info, cb) => { - // Reject connections if we aren't synced with the database - if (!this._reql_conn.is_ready()) { - cb(false, 503, 'Connection to the database is down.'); - } else { - cb(true); - } - }; - - const ws_options = { handleProtocols: accept_protocol, - allowRequest: verify_client, - path: this._path }; - - const add_websocket = (server) => { - const ws_server = new websocket.Server(Object.assign({ server }, ws_options)) + // RSI: only become ready when this and metadata are both ready. + const add_websocket = (server) => { + const ws_server = new websocket.Server(Object.assign({ server }, ws_options)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => new Client(socket, this)); - this._ws_servers.push(ws_server); - }; + this._ws_servers.push(ws_server); + }; - if (http_servers.forEach === undefined) { - add_websocket(http_servers); - } else { - http_servers.forEach((s) => add_websocket(s)); - } - } catch (err) { - this._interrupt(err); - throw err; + if (http_servers.forEach === undefined) { + add_websocket(http_servers); + } else { + http_servers.forEach((s) => add_websocket(s)); } } - add_middleware(mw) { - this._middlewares.push(mw); + metadata() { + return this._reliable_metadata; } - + conn() { + return this._reliable_conn; + } - ready() { - return this._reql_conn.ready().then(() => this); + add_middleware(mw) { + this._middlewares.push(mw); } + // TODO: We close clients in `onUnready` above, but don't wait for + // them to be closed. close() { if (!this._close_promise) { - this._interrupt(new Error('Horizon server is shutting down.')); - this._close_promise = Promise.all([ - Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { + this._close_promise = this._reliable_metadata.close().then(() => { + return Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { s.close(resolve); - }))), - this._reql_conn.ready().catch(() => { }), - ]); + }))).then(() => { + return this._reliable_conn.close(); + }); + }); } return this._close_promise; } } + + module.exports = { Server, protocol: protocol_name, From 8cfac85cf9bab3b4acc6d6d3af39e87e696c5c5a Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Mon, 15 Aug 2016 22:24:57 +0000 Subject: [PATCH 06/86] Reworked reliable stuff. --- plugins/permissions/src/permissions.js | 2 - server/src/reliable_cfeed.js | 151 +++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 server/src/reliable_cfeed.js diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index d32931249..5f7f6e399 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -6,8 +6,6 @@ module.exports = (config) => { name: 'permissions', activate: (ctx) => { - - ctx.logger.info('Activating permissions.'); return { middleware: (req, res, next) => { diff --git a/server/src/reliable_cfeed.js b/server/src/reliable_cfeed.js new file mode 100644 index 000000000..4599380b1 --- /dev/null +++ b/server/src/reliable_cfeed.js @@ -0,0 +1,151 @@ +'use strict'; + +class Reliable { + constructor(initialCbs) { + this.subs = {}; + this.ready = false; + this.closed = false; + if (initialCbs) { + subscribe(initialCbs); + } + } + + subscribe(cbs) { + if (this.closed) { + throw new Error("Cannot subscribe to a closed ReliableConn."); + } + const subId = Symbol(); + subs[subId] = { + cbs: cbs, + close: () => delete this.subs[subId], + }; + if (this.ready && cbs.onReady) { + try { + cbs.onReady.apply(cbs, this.ready); + } catch (e) { + // log e + } + } + return subs[subId]; + } + + emit() { + if (!this.closed) { + emitInternal.apply(this, arguments); + } + } + + emitInternal() { + const eventType = arguments.shift(); + // TODO: consider checking to make sure we don't send two + // `onReady` or `onUnready`s in a row (or even just returning + // early if we would). + if (eventType == "onReady") { + this.ready = arguments; + } else if (eventType == "onUnready") { + this.ready = false; + } + for (let s of Object.getOwnPropertySymbols(this.subs)) { + try { + const cbs = this.subs[s].cbs; + const event = cbs[eventType]; + if (event) { event.apply(cbs, arguments); } + } catch (e) { + // log e + } + } + } + + close(reason) { + this.closed = true; + if (this.ready) { + emitInternal('onUnready', new Error('closed: ' + reason)); + } + this.subs = {}; // Just to make sure no subclasses do anything clever. + return Promise.resolve(); + } +} + +class ReliableConn extends Reliable{ + constructor(connOpts) { + super(); + this.connOpts = connOpts; + connect(); + } + + connect() { + r.connect(this.connOpts).then((conn) => { + if (!this.closed) { + this.conn = conn; + emit('onReady', conn); + conn.on('close', () => { + if (this.ready) { + emit('onUnready', new Error('connection closed')); + if (!this.closed) { + connect(); + } + } + }) + } else { + conn.close(); + } + }).catch((e) => { + if (this.conn) { + // RSI: log a scary error. + } + if (!this.closed) { + setTimeout(1000, connect); + } + }) + } + + close(reason) { + let retProm = super.close(reason); + if (this.conn) { + retProm = Promise.all([retProm, this.conn.close()]); + } + return retProm; + } +} + +class ReliableCfeed extends Reliable { + constructor(reql, reliableConn, cbs) { + super(cbs); + this.reql = reql; + this.reliableConn = reliableConn; + + // RSI: restart this if there's an error on the cfeed rather than the connection. + this.subscription = reliableConn.subscribe({ + onReady: (conn) => { + reql.run(conn, {includeTypes: true, includeStates: true}).then((cursor) => { + this.cursor = cursor; + return cursor.eachAsync(change) { + switch (change.type) { + case 'state': + if (change.state === 'ready') { + emit('onReady'); + } + break; + default: + emit('onChange', change); + } + } + }).then((res) => { + // If we get here the cursor closed for some reason. + throw new Error("cursor closed for some reason"); + }).catch((e) => { + emit('onUnready', e); + }); + } + }); + } + + close(reason) { + let retProm = super.close(reason); + if (this.cursor) { + retProm = Promise.all([retProm, this.cursor.close()]); + } + this.subscription.close(); + return retProm; + } +} From 1378c09d8f35e0c61d9cbc56a539b83dc5b4bb75 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Mon, 15 Aug 2016 19:23:48 -0700 Subject: [PATCH 07/86] added ReliableMetadata sketch --- server/src/metadata/reliable_metadata.js | 363 +++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 server/src/metadata/reliable_metadata.js diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js new file mode 100644 index 000000000..3baf6ac9c --- /dev/null +++ b/server/src/metadata/reliable_metadata.js @@ -0,0 +1,363 @@ +'use strict'; + +const error = require('../error'); +const logger = require('../logger'); +const Collection = require('./collection').Collection; +const utils = require('../utils'); + +const r = require('rethinkdb'); + +const metadata_version = [ 2, 0, 0 ]; + +const create_collection = (db, name, conn) => + r.db(db).table('hz_collections').get(name).replace({ id: name }).do((res) => + r.branch( + res('errors').ne(0), + r.error(res('first_error')), + res('inserted').eq(1), + r.db(db).tableCreate(name), + res + ) + ).run(conn); + +const initialize_metadata = (db, conn) => + r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) + .then(() => + Promise.all([ 'hz_collections', 'hz_users_auth', 'hz_groups' ].map((table) => + r.branch(r.db(db).tableList().contains(table), + { }, + r.db(db).tableCreate(table)) + .run(conn)))) + .then(() => + r.db(db).table('hz_collections').wait({ timeout: 30 }).run(conn)) + .then(() => + Promise.all([ + r.db(db).tableList().contains('users').not().run(conn).then(() => + create_collection(db, 'users', conn)), + r.db(db).table('hz_collections') + .insert({ id: 'hz_metadata', version: metadata_version }) + .run(conn), + ]) + ); + +class StaleAttemptError extends Error { } + +class ReliableInit extends Reliable { + constructor(db, reliable_conn) { + super(); + this._db = db; + this._conn_subs = reliable_conn.subscribe({ + onReady: (conn) => { + this.current_attempt = Symbol(); + do_init(conn); + }, + onUnready: () => { + this.current_attempt = null; + if (this.ready) { + this.emit('onUnready'); + } + }, + }); + } + + check_attempt(attempt) { + if (attempt !== this.current_attempt) { + throw new StaleAttemptError(); + } + } + + do_init(conn, attempt) { + Promise.resolve().then(() => { + this.check_attempt(attempt); + logger.debug('checking rethinkdb version'); + const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version') + return q.run(conn).then((res) => utils.rethinkdb_version_check(res)); + }).then(() => { + this.check_attempt(attempt); + logger.debug('checking for old metadata version'); + const old_metadata_db = `${this._db}_internal`; + return r.dbList().contains(old_metadata_db).run(conn).then((has_old_db) => { + if (has_old_db) { + throw new Error('The Horizon metadata appears to be from v1.x because ' + + `the "${old_metadata_db}" database exists. Please use ` + + '`hz migrate` to convert your metadata to the new format.'); + } + }); + }).then(() => { + this.check_attempt(attempt); + logger.debug('checking for internal tables'); + if (this._auto_create_collection) { + return initialize_metadata(this._db, conn); + } else { + return r.dbList().contains(this._db).run(conn).then((has_db) => { + if (!has_db) { + throw new Error(`The database ${this._db} does not exist. ` + + 'Run `hz schema apply` to initialize the database, ' + + 'then start the Horizon server.'); + } + }); + } + }).then(() => { + this.check_attempt(attempt); + logger.debug('waiting for internal tables'); + return r.expr([ 'hz_collections', 'hz_users_auth', 'hz_groups', 'users' ]) + .forEach((table) => r.db(this._db).table(table).wait({ timeout: 30 })).run(conn); + }).then(() => { + this.check_attempt(attempt); + logger.debug('adding admin user'); + return Promise.all([ + r.db(this._db).table('users').get('admin') + .replace((old_row) => + r.branch(old_row.eq(null), + { + id: 'admin', + groups: [ 'admin' ], + [version_field]: 0, + }, + old_row), + { returnChanges: 'always' })('changes')(0) + .do((res) => + r.branch(res('new_val').eq(null), + r.error(res('error')), + res('new_val'))).run(this._conn), + r.db(this._db).table('hz_groups').get('admin') + .replace((old_row) => + r.branch(old_row.eq(null), + { + id: 'admin', + rules: { carte_blanche: { template: 'any()' } }, + [version_field]: 0, + }, + old_row), + { returnChanges: 'always' })('changes')(0) + .do((res) => + r.branch(res('new_val').eq(null), + r.error(res('error')), + res('new_val'))).run(this._conn), + ]); + }).then(() => { + this.check_attempt(attempt); + logger.debug('metadata sync complete'); + this.emit('onReady'); + }).catch((err) => { + if (!(err instanceof StaleAttemptError)) { + logger.debug(`Metadata initialization failed: ${err}`); + setTimeout(1000, () => { this.do_init(conn, attempt); }); + } + }); + } + + close(reason) { + this.current_attempt = null; + super.close(reason); + this._conn_subs.close(reason); + } +} + +class ReliableMetadata extends Reliable { + constructor(project_name, + reliable_conn, + auto_create_collection, + auto_create_index) { + super(); + + this._db = project_name; + this._reliable_conn = reliable_conn; + this._collections = new Map(); + + this._reliable_init = new ReliableInit(reliable_conn); + + this._collection_changefeed = new ReliableChangefeed( + r.db(this._db) + .table('hz_collections') + .filter((row) => row('id').match('^hzp?_').not()) + .changes({ squash: false, includeInitial: true, includeTypes: true }), + reliable_conn, + { + onChange: (change) => { + switch (change.type) { + case 'initial': + case 'add': + case 'change': + { + const collection_name = change.new_val.id; + let collection = this._collections.get(collection_name); + if (!collection) { + collection = new Collection(this._db, collection_name); + this._collections.set(collection_name, collection); + } + collection._register(); + } + break; + case 'uninitial': + case 'remove': + { + const collection_name = change.new_val.id; + const collection = this._collections.get(collection_name); + if (collection) { + collection._unregister(); + if (collection._is_safe_to_remove()) { + this._collections.delete(collection_name); + collection.close(); + } + } + } + break; + default: + // log error + break; + } + }, + }); + + + this._index_changefeed = new ReliableChangefeed( + r.db('rethinkdb') + .table('table_config') + .filter((row) => r.and(row('db').eq(this._db), + row('name').match('^hzp?_').not())) + .map((row) => ({ + id: row('id'), + name: row('name'), + indexes: row('indexes').filter((idx) => idx.match('^hz_')), + })) + .changes({ squash: true, + includeInitial: true, + includeStates: true, + includeTypes: true }) + r.db(this._db) + .table('hz_collections') + .filter((row) => row('id').match('^hzp?_').not()) + .changes({ squash: false, includeInitial: true, includeTypes: true }), + reliable_conn, + { + onChange: (change) => { + switch (change.type) { + case 'initial': + case 'add': + case 'change': + { + const collection_name = change.new_val.name; + const table_id = change.new_val.id; + + let collection = this._collections.get(collection_name); + if (!collection) { + collection = new Collection(this._db, collection_name); + this._collections.set(collection_name, collection); + } + collection._update_table(table_id, change.new_val.indexes, this._reliable_conn.connection()); + } + break; + case 'uninitial': + case 'remove': + { + const collection = this._collections.get(change.old_val.name); + if (collection) { + collection._update_table(change.old_val.id, null, this._reliable_conn.connection()); + if (collection._is_safe_to_remove()) { + this._collections.delete(collection); + collection._close(); + } + } + } + break; + default: + // log error + break; + } + }, + }); + + this._ready_union = new ReliableUnion([ + this._reliable_init, + this._collection_changefeed, + this._index_changefeed, + ], { + onReady: () => { + this.emit('onReady'); + }, + onUnready: () => { + this._collections.forEach((collection) => collection.close(reason)); + this._collections.clear(); + }, + }); + } + + close(reason) { + super.close(reason); + + this._reliable_init.close(reason); + this._collection_changefeed.close(reason); + this._index_changefeed.close(reason); + this._ready_union.close(reason); + } + + // Public interface for use by plugins or other classes + collection(name) { + if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { + throw new Error(`Collection "${name}" is reserved for internal use ` + + 'and cannot be used in requests.'); + } else if (!this.ready) { + throw new Error('ReliableMetadata is not ready.'); + } + + const collection = this._collections.get(name); + if (!collection) { throw new error.CollectionMissing(name); } + if (!collection._get_table().ready()) { throw new error.CollectionNotReady(collection); } + return collection; + } + + create_collection(name, done) { + if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { + throw new Error(`Collection "${name}" is reserved for internal use ` + + 'and cannot be used in requests.'); + } else if (!this.ready) { + throw new Error('ReliableMetadata is not ready.'); + } else if (this._collections.get(name)) { + throw new Error(`Collection "${name}" already exists.`); + } + + const collection = new Collection(this._db, name); + this._collections.set(name, collection); + + create_collection(this._db, name, this._reliable_conn.connection()).then((res) => { + error.check(!res.error, `Collection "${name}" creation failed: ${res.error}`); + logger.warn(`Collection created: "${name}"`); + collection._on_ready(done); + }).catch((err) => { + if (collection._is_safe_to_remove()) { + this._collections.delete(name); + collection._close(); + } + done(err); + }); + } + + handle_error(err, done) { + logger.debug(`Handling error: ${err.message}`); + try { + if (err instanceof error.CollectionNotReady) { + return err.collection._on_ready(done); + } else if (err instanceof error.IndexNotReady) { + return err.index.on_ready(done); + } else if (this._auto_create_collection && (err instanceof error.CollectionMissing)) { + logger.warn(`Auto-creating collection: ${err.name}`); + return this.create_collection(err.name, done); + } else if (this._auto_create_index && (err instanceof error.IndexMissing)) { + logger.warn(`Auto-creating index on collection "${err.collection.name}": ` + + `${JSON.stringify(err.fields)}`); + return err.collection._create_index(err.fields, this._reliable_conn.connection(), done); + } + done(err); + } catch (new_err) { + logger.debug(`Error when handling error: ${new_err.message}`); + done(new_err); + } + } + + reliable_connection() { + return this._reliable_conn; + } +} + +module.exports = { ReliableMetadata, create_collection, initialize_metadata }; From bd146d1117798044275622a8c4fcfc90bfa30d27 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Tue, 16 Aug 2016 02:56:03 +0000 Subject: [PATCH 08/86] Added reliable shit. --- server/src/metadata/metadata.js | 1 - server/src/metadata/reliable_metadata.js | 15 +++-- server/src/{reliable_cfeed.js => reliable.js} | 64 +++++++++++++++---- 3 files changed, 60 insertions(+), 20 deletions(-) rename server/src/{reliable_cfeed.js => reliable.js} (69%) diff --git a/server/src/metadata/metadata.js b/server/src/metadata/metadata.js index 76e8ba11b..aaa279d1c 100644 --- a/server/src/metadata/metadata.js +++ b/server/src/metadata/metadata.js @@ -42,7 +42,6 @@ const initialize_metadata = (db, conn) => ]) ); -// RSI: pick up here, make this reliable. class Metadata { constructor(project_name, reliable_conn, diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index 3baf6ac9c..d01101aba 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -43,13 +43,14 @@ const initialize_metadata = (db, conn) => class StaleAttemptError extends Error { } class ReliableInit extends Reliable { - constructor(db, reliable_conn) { + constructor(db, reliable_conn, auto_create_collection) { super(); this._db = db; + this._auto_create_collection = auto_create_collection; this._conn_subs = reliable_conn.subscribe({ onReady: (conn) => { this.current_attempt = Symbol(); - do_init(conn); + do_init(conn, this.current_attempt); }, onUnready: () => { this.current_attempt = null; @@ -268,11 +269,11 @@ class ReliableMetadata extends Reliable { }, }); - this._ready_union = new ReliableUnion([ - this._reliable_init, - this._collection_changefeed, - this._index_changefeed, - ], { + this._ready_union = new ReliableUnion({ + reliable_init: this._reliable_init, + collection_changefeed: this._collection_changefeed, + index_changefeed: this._index_changefeed, + }, { onReady: () => { this.emit('onReady'); }, diff --git a/server/src/reliable_cfeed.js b/server/src/reliable.js similarity index 69% rename from server/src/reliable_cfeed.js rename to server/src/reliable.js index 4599380b1..416ac1ae8 100644 --- a/server/src/reliable_cfeed.js +++ b/server/src/reliable.js @@ -6,7 +6,7 @@ class Reliable { this.ready = false; this.closed = false; if (initialCbs) { - subscribe(initialCbs); + this.subscribe(initialCbs); } } @@ -31,7 +31,7 @@ class Reliable { emit() { if (!this.closed) { - emitInternal.apply(this, arguments); + this.emitInternal.apply(this, arguments); } } @@ -51,7 +51,7 @@ class Reliable { const event = cbs[eventType]; if (event) { event.apply(cbs, arguments); } } catch (e) { - // log e + // TODO: log e } } } @@ -59,7 +59,7 @@ class Reliable { close(reason) { this.closed = true; if (this.ready) { - emitInternal('onUnready', new Error('closed: ' + reason)); + this.emitInternal('onUnready', new Error(`closed: ${reason}`)); } this.subs = {}; // Just to make sure no subclasses do anything clever. return Promise.resolve(); @@ -70,19 +70,19 @@ class ReliableConn extends Reliable{ constructor(connOpts) { super(); this.connOpts = connOpts; - connect(); + this.connect(); } connect() { r.connect(this.connOpts).then((conn) => { if (!this.closed) { this.conn = conn; - emit('onReady', conn); + this.emit('onReady', conn); conn.on('close', () => { if (this.ready) { - emit('onUnready', new Error('connection closed')); + this.emit('onUnready', new Error('connection closed')); if (!this.closed) { - connect(); + this.connect(); } } }) @@ -94,7 +94,7 @@ class ReliableConn extends Reliable{ // RSI: log a scary error. } if (!this.closed) { - setTimeout(1000, connect); + setTimeout(1000, () => this.connect()); } }) } @@ -123,18 +123,18 @@ class ReliableCfeed extends Reliable { switch (change.type) { case 'state': if (change.state === 'ready') { - emit('onReady'); + this.emit('onReady'); } break; default: - emit('onChange', change); + this.emit('onChange', change); } } }).then((res) => { // If we get here the cursor closed for some reason. throw new Error("cursor closed for some reason"); }).catch((e) => { - emit('onUnready', e); + this.emit('onUnready', e); }); } }); @@ -149,3 +149,43 @@ class ReliableCfeed extends Reliable { return retProm; } } + +class ReliableUnion extends Reliable { + constructor(reqs, cbs) { + super(cbs); + this.reqs = reqs; + this.subs = {}; + this.emitArg = {}; + this.readyNeeded = 0; + for (const k in reqs) { + this.subs[k] = reqs[k].subscribe({ + onReady: (...rest) => { + this.readyNeeded -= 1; + this.emitArg[k] = rest; + this.maybeEmit(); + }, + onUnready: (...rest) => { + this.readyNeeded += 1; + this.emitArg[k] = rest; + this.maybeEmit(); + }, + }); + this.readyNeeded += 1; + } + } + + maybeEmit() { + if (this.readyNeeded === 0 && !this.ready) { + this.emit('onReady', emitArg); + } else if (this.readyNeeded !== 0 && this.ready) { + this.emit('onUnready', emitArg); + } + } + + close(reason) { + for (const k in this.subs) { + this.subs[k].close(); + } + return super.close(reason); + } +} From 2143e8c5fc2226aac0266ba841b75e024035a32e Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Tue, 16 Aug 2016 03:09:47 +0000 Subject: [PATCH 09/86] style fixes --- .eslintrc.js | 2 +- server/src/auth.js | 18 +- server/src/auth/auth0.js | 22 +- server/src/auth/facebook.js | 20 +- server/src/auth/github.js | 18 +- server/src/auth/google.js | 14 +- server/src/auth/slack.js | 2 +- server/src/auth/twitch.js | 16 +- server/src/auth/twitter.js | 14 +- server/src/auth/utils.js | 12 +- server/src/client.js | 26 +- server/src/endpoint/insert.js | 4 +- server/src/endpoint/query.js | 14 +- server/src/endpoint/remove.js | 8 +- server/src/endpoint/replace.js | 4 +- server/src/endpoint/store.js | 6 +- server/src/endpoint/subscribe.js | 12 +- server/src/endpoint/update.js | 4 +- server/src/endpoint/upsert.js | 6 +- server/src/endpoint/writes.js | 17 +- server/src/horizon.js | 2 +- server/src/metadata/collection.js | 2 +- server/src/metadata/index.js | 6 +- server/src/metadata/metadata.js | 374 ----------------------- server/src/metadata/reliable_metadata.js | 40 ++- server/src/metadata/table.js | 8 +- server/src/permissions/group.js | 2 +- server/src/permissions/rule.js | 4 +- server/src/permissions/template.js | 8 +- server/src/permissions/validator.js | 2 +- server/src/reql_connection.js | 6 +- server/src/response.js | 2 +- server/src/schema/horizon_protocol.js | 12 +- server/src/schema/server_options.js | 2 +- 34 files changed, 165 insertions(+), 544 deletions(-) delete mode 100644 server/src/metadata/metadata.js diff --git a/.eslintrc.js b/.eslintrc.js index c28d18b71..642ace597 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -68,7 +68,7 @@ module.exports = { "no-var": [ ERROR ], "no-void": [ ERROR ], "no-with": [ ERROR ], - "object-curly-spacing": [ ERROR, "always" ], + "object-curly-spacing": [ ERROR, "never" ], "one-var": [ ERROR, { "uninitialized": "always", "initialized": "never" } ], "operator-assignment": [ ERROR, "always" ], "operator-linebreak": [ ERROR, "after" ], diff --git a/server/src/auth.js b/server/src/auth.js index 13f9ed41d..d87b5f556 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -31,15 +31,15 @@ class JWT { const token = jwt.sign( payload, this.secret, - { algorithm: this.algorithm, expiresIn: this.duration } + {algorithm: this.algorithm, expiresIn: this.duration} ); - return { token, payload }; + return {token, payload}; } verify(token) { - return jwt.verifyAsync(token, this.secret, { algorithms: [ this.algorithm ] }) - .then((payload) => ({ token, payload })); + return jwt.verifyAsync(token, this.secret, {algorithms: [ this.algorithm ]}) + .then((payload) => ({token, payload})); } } @@ -68,7 +68,7 @@ class Auth { if (!this._allow_unauthenticated) { throw new Error('Unauthenticated connections are not allowed.'); } - return this._jwt.verify(this._jwt.sign({ id: null, provider: request.method }).token); + return this._jwt.verify(this._jwt.sign({id: null, provider: request.method}).token); case 'anonymous': if (!this._allow_anonymous) { throw new Error('Anonymous connections are not allowed.'); @@ -103,7 +103,7 @@ class Auth { const insert = (table, row) => db.table(table) - .insert(row, { conflict: 'error', returnChanges: 'always' }) + .insert(row, {conflict: 'error', returnChanges: 'always'}) .bracket('changes')(0)('new_val'); let query = db.table('users') @@ -111,7 +111,7 @@ class Auth { .default(r.error('User not found and new user creation is disabled.')); if (this._create_new_users) { - query = insert('hz_users_auth', { id: key, user_id: r.uuid() }) + query = insert('hz_users_auth', {id: key, user_id: r.uuid()}) .do((auth_user) => insert('users', this.new_user_row(auth_user('user_id')))); } @@ -121,9 +121,9 @@ class Auth { logger.debug(`Failed user lookup or creation: ${err}`); throw new Error('User lookup or creation in database failed.'); }).then((user) => - this._jwt.sign({ id: user.id, provider })); + this._jwt.sign({id: user.id, provider})); } } -module.exports = { Auth }; +module.exports = {Auth}; diff --git a/server/src/auth/auth0.js b/server/src/auth/auth0.js index 834697e07..f80831830 100644 --- a/server/src/auth/auth0.js +++ b/server/src/auth/auth0.js @@ -1,7 +1,6 @@ 'use strict'; const auth_utils = require('./utils'); -const logger = require('../logger'); const https = require('https'); const querystring = require('querystring'); @@ -22,28 +21,25 @@ function auth0(horizon, raw_options) { const client_secret = options.secret; const host = options.host; - const self_url = (self_host, path) => - url.format({ protocol: 'https', host: self_host, pathname: path }); - const make_acquire_url = (state, redirect_uri) => - url.format({ protocol: 'https', + url.format({protocol: 'https', host: host, pathname: '/authorize', - query: { response_type: 'code', client_id, redirect_uri, state } }); + query: {response_type: 'code', client_id, redirect_uri, state}}); const make_token_request = (code, redirect_uri) => { - const req = https.request({ method: 'POST', host, path: '/oauth/token', - headers: { 'Content-type': 'application/x-www-form-urlencoded' } }); + const req = https.request({method: 'POST', host, path: '/oauth/token', + headers: {'Content-type': 'application/x-www-form-urlencoded'}}); req.write(querystring.stringify({ - client_id, redirect_uri, client_secret, code, - grant_type: 'authorization_code' - })); + client_id, redirect_uri, client_secret, code, + grant_type: 'authorization_code', + })); return req; }; const make_inspect_request = (access_token) => - https.request({ host, path: '/userinfo', - headers: { Authorization: `Bearer ${access_token}` } }); + https.request({host, path: '/userinfo', + headers: {Authorization: `Bearer ${access_token}`}}); const extract_id = (user_info) => user_info && user_info.identities[0].user_id; diff --git a/server/src/auth/facebook.js b/server/src/auth/facebook.js index 10d0eb7cf..a92a99626 100644 --- a/server/src/auth/facebook.js +++ b/server/src/auth/facebook.js @@ -25,10 +25,10 @@ function facebook(horizon, raw_options) { const make_app_token_request = () => https.request( - url.format({ protocol: 'https', + url.format({protocol: 'https', host: 'graph.facebook.com', pathname: '/oauth/access_token', - query: { client_id, client_secret, grant_type: 'client_credentials' } })); + query: {client_id, client_secret, grant_type: 'client_credentials'}})); auth_utils.run_request(make_app_token_request(), (err, body) => { const parsed = body && querystring.parse(body); @@ -41,28 +41,28 @@ function facebook(horizon, raw_options) { } }); - const oauth_options = { horizon, provider }; + const oauth_options = {horizon, provider}; oauth_options.make_acquire_url = (state, redirect_uri) => - url.format({ protocol: 'https', + url.format({protocol: 'https', host: 'www.facebook.com', pathname: '/dialog/oauth', - query: { client_id, state, redirect_uri, response_type: 'code' } }); + query: {client_id, state, redirect_uri, response_type: 'code'}}); oauth_options.make_token_request = (code, redirect_uri) => { - const req = https.request({ method: 'POST', + const req = https.request({method: 'POST', host: 'graph.facebook.com', - path: '/v2.3/oauth/access_token' }); - req.write(querystring.stringify({ code, redirect_uri, client_id, client_secret })); + path: '/v2.3/oauth/access_token'}); + req.write(querystring.stringify({code, redirect_uri, client_id, client_secret})); return req; }; oauth_options.make_inspect_request = (input_token) => https.request( - url.format({ protocol: 'https', + url.format({protocol: 'https', host: 'graph.facebook.com', pathname: '/debug_token', - query: { access_token: app_token, input_token } })); + query: {access_token: app_token, input_token}})); oauth_options.extract_id = (user_info) => user_info && user_info.data && user_info.data.user_id; diff --git a/server/src/auth/github.js b/server/src/auth/github.js index 84bfc79bc..251286af5 100644 --- a/server/src/auth/github.js +++ b/server/src/auth/github.js @@ -19,29 +19,29 @@ function github(horizon, raw_options) { const client_secret = options.secret; const provider = options.path; - const oauth_options = { horizon, provider }; + const oauth_options = {horizon, provider}; oauth_options.make_acquire_url = (state, redirect_uri) => - url.format({ protocol: 'https', + url.format({protocol: 'https', host: 'github.com', pathname: '/login/oauth/authorize', - query: { client_id, redirect_uri, state } }); + query: {client_id, redirect_uri, state}}); oauth_options.make_token_request = (code, redirect_uri) => { - const req = https.request({ method: 'POST', + const req = https.request({method: 'POST', host: 'github.com', path: '/login/oauth/access_token', - headers: { accept: 'application/json' } }); + headers: {accept: 'application/json'}}); - req.write(querystring.stringify({ code, client_id, client_secret, redirect_uri })); + req.write(querystring.stringify({code, client_id, client_secret, redirect_uri})); return req; }; oauth_options.make_inspect_request = (access_token) => - https.request({ host: 'api.github.com', - path: `/user?${querystring.stringify({ access_token })}`, - headers: { 'user-agent': 'node.js' } }); + https.request({host: 'api.github.com', + path: `/user?${querystring.stringify({access_token})}`, + headers: {'user-agent': 'node.js'}}); oauth_options.extract_id = (user_info) => user_info && user_info.id; diff --git a/server/src/auth/google.js b/server/src/auth/google.js index 3af34c164..dad305b3b 100644 --- a/server/src/auth/google.js +++ b/server/src/auth/google.js @@ -20,26 +20,26 @@ function google(horizon, raw_options) { const client_secret = options.secret; const provider = options.path; - const oauth_options = { horizon, provider }; + const oauth_options = {horizon, provider}; oauth_options.make_acquire_url = (state, redirect_uri) => - url.format({ protocol: 'https', + url.format({protocol: 'https', host: 'accounts.google.com', pathname: '/o/oauth2/v2/auth', - query: { client_id, redirect_uri, state, response_type: 'code', scope: 'profile' } }); + query: {client_id, redirect_uri, state, response_type: 'code', scope: 'profile'}}); oauth_options.make_token_request = (code, redirect_uri) => { const query_params = querystring.stringify({ code, client_id, client_secret, redirect_uri, - grant_type: 'authorization_code' }); + grant_type: 'authorization_code'}); const path = `/oauth2/v4/token?${query_params}`; - return https.request({ method: 'POST', host: 'www.googleapis.com', path }); + return https.request({method: 'POST', host: 'www.googleapis.com', path}); }; oauth_options.make_inspect_request = (access_token) => { logger.debug(`using access token: ${access_token}`); - const path = `/oauth2/v1/userinfo?${querystring.stringify({ access_token })}`; - return https.request({ host: 'www.googleapis.com', path }); + const path = `/oauth2/v1/userinfo?${querystring.stringify({access_token})}`; + return https.request({host: 'www.googleapis.com', path}); }; oauth_options.extract_id = (user_info) => user_info && user_info.id; diff --git a/server/src/auth/slack.js b/server/src/auth/slack.js index ec06b3a42..67919898e 100644 --- a/server/src/auth/slack.js +++ b/server/src/auth/slack.js @@ -59,7 +59,7 @@ function slack(horizon, raw_options) { oauth_options.make_inspect_request = (access_token) => https.request({ host: 'slack.com', - path: `/api/auth.test?${querystring.stringify({ token: access_token })}`, + path: `/api/auth.test?${querystring.stringify({token: access_token})}`, headers: { 'Content-Type': 'application/json', 'user-agent': 'node.js', diff --git a/server/src/auth/twitch.js b/server/src/auth/twitch.js index c2c802169..eb12f14ec 100644 --- a/server/src/auth/twitch.js +++ b/server/src/auth/twitch.js @@ -20,29 +20,29 @@ function twitch(horizon, raw_options) { const client_secret = options.secret; const provider = options.path; - const oauth_options = { horizon, provider }; + const oauth_options = {horizon, provider}; oauth_options.make_acquire_url = (state, redirect_uri) => - url.format({ protocol: 'https', + url.format({protocol: 'https', host: 'api.twitch.tv', pathname: '/kraken/oauth2/authorize', - query: { client_id, redirect_uri, state, response_type: 'code', scope: 'user_read' } }); + query: {client_id, redirect_uri, state, response_type: 'code', scope: 'user_read'}}); oauth_options.make_token_request = (code, redirect_uri) => { - const req = https.request({ method: 'POST', + const req = https.request({method: 'POST', host: 'api.twitch.tv', - path: '/kraken/oauth2/token' }); + path: '/kraken/oauth2/token'}); req.write(querystring.stringify({ client_id, redirect_uri, client_secret, code, - grant_type: 'authorization_code' })); + grant_type: 'authorization_code'})); return req; }; oauth_options.make_inspect_request = (access_token) => { logger.debug(`using access token: ${access_token}`); - return https.request({ host: 'api.twitch.tv', + return https.request({host: 'api.twitch.tv', path: '/kraken/user', - headers: { authorization: `OAuth ${access_token}` } }); + headers: {authorization: `OAuth ${access_token}`}}); }; oauth_options.extract_id = (user_info) => user_info && user_info._id; diff --git a/server/src/auth/twitter.js b/server/src/auth/twitter.js index 4e9643f42..cb315347f 100644 --- a/server/src/auth/twitter.js +++ b/server/src/auth/twitter.js @@ -28,7 +28,7 @@ const store_app_token = (nonce, token) => { item = iter.next(); } - nonce_cache.set(nonce, { time, token }); + nonce_cache.set(nonce, {time, token}); }; const get_app_token = (nonce) => { @@ -54,10 +54,10 @@ function twitter(horizon, raw_options) { const user_info_url = 'https://api.twitter.com/1.1/account/verify_credentials.json'; const make_success_url = (horizon_token) => - url.format(auth_utils.extend_url_query(horizon._auth._success_redirect, { horizon_token })); + url.format(auth_utils.extend_url_query(horizon._auth._success_redirect, {horizon_token})); const make_failure_url = (horizon_error) => - url.format(auth_utils.extend_url_query(horizon._auth._failure_redirect, { horizon_error })); + url.format(auth_utils.extend_url_query(horizon._auth._failure_redirect, {horizon_error})); horizon.add_http_handler(provider, (req, res) => { const request_url = url.parse(req.url, true); @@ -74,10 +74,10 @@ function twitter(horizon, raw_options) { auth_utils.do_redirect(res, make_failure_url('error generating nonce')); } else { oa._authorize_callback = - url.format({ protocol: 'https', + url.format({protocol: 'https', host: req.headers.host, pathname: request_url.pathname, - query: { state: auth_utils.nonce_to_state(nonce) } }); + query: {state: auth_utils.nonce_to_state(nonce)}}); oa.getOAuthRequestToken((err, app_token, app_token_secret, body) => { if (err || body.oauth_callback_confirmed !== 'true') { @@ -86,10 +86,10 @@ function twitter(horizon, raw_options) { } else { store_app_token(nonce, app_token_secret); auth_utils.set_nonce(res, horizon._name, nonce); - auth_utils.do_redirect(res, url.format({ protocol: 'https', + auth_utils.do_redirect(res, url.format({protocol: 'https', host: 'api.twitter.com', pathname: '/oauth/authenticate', - query: { oauth_token: app_token } })); + query: {oauth_token: app_token}})); } }); } diff --git a/server/src/auth/utils.js b/server/src/auth/utils.js index c95ba45a9..0f6711790 100644 --- a/server/src/auth/utils.js +++ b/server/src/auth/utils.js @@ -9,7 +9,7 @@ const url = require('url'); const do_redirect = (res, redirect_url) => { logger.debug(`Redirecting user to ${redirect_url}`); - res.writeHead(302, { Location: redirect_url }); + res.writeHead(302, {Location: redirect_url}); res.end(); }; @@ -71,12 +71,12 @@ const nonce_to_state = (nonce) => const set_nonce = (res, name, nonce) => res.setHeader('set-cookie', cookie.serialize(nonce_cookie(name), nonce, - { maxAge: 3600, secure: true, httpOnly: true })); + {maxAge: 3600, secure: true, httpOnly: true})); const clear_nonce = (res, name) => res.setHeader('set-cookie', cookie.serialize(nonce_cookie(name), 'invalid', - { maxAge: -1, secure: true, httpOnly: true })); + {maxAge: -1, secure: true, httpOnly: true})); const get_nonce = (req, name) => { const field = nonce_cookie(name); @@ -107,13 +107,13 @@ const oauth2 = (raw_options) => { const extract_id = options.extract_id; const self_url = (host, path) => - url.format({ protocol: 'https', host: host, pathname: path }); + url.format({protocol: 'https', host: host, pathname: path}); const make_success_url = (horizon_token) => - url.format(extend_url_query(horizon._auth._success_redirect, { horizon_token })); + url.format(extend_url_query(horizon._auth._success_redirect, {horizon_token})); const make_failure_url = (horizon_error) => - url.format(extend_url_query(horizon._auth._failure_redirect, { horizon_error })); + url.format(extend_url_query(horizon._auth._failure_redirect, {horizon_error})); horizon.add_http_handler(provider, (req, res) => { const request_url = url.parse(req.url, true); diff --git a/server/src/client.js b/server/src/client.js index 70f837860..abbcc5efa 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -31,7 +31,7 @@ class Client { this.error_wrap_socket(() => this.handle_handshake(data))); if (!this._metadata.is_ready()) { - this.close({ error: 'No connection to the database.' }); + this.close({error: 'No connection to the database.'}); } } @@ -56,9 +56,9 @@ class Client { cb(); } catch (err) { logger.debug(`Unhandled error in request: ${err.stack}`); - this.close({ request_id: null, + this.close({request_id: null, error: `Unhandled error: ${err}`, - error_code: 0 }); + error_code: 0}); } } @@ -67,9 +67,9 @@ class Client { try { request = JSON.parse(data); } catch (err) { - return this.close({ request_id: null, + return this.close({request_id: null, error: `Invalid JSON: ${err}`, - error_code: 0 }); + error_code: 0}); } try { @@ -81,9 +81,9 @@ class Client { if (request.request_id === undefined) { // This is pretty much an unrecoverable protocol error, so close the connection - this.close({ request_id, error: `Protocol error: ${err}`, error_code: 0 }); + this.close({request_id, error: `Protocol error: ${err}`, error_code: 0}); } else { - this.send_error({ request_id }, err_str); + this.send_error({request_id}, err_str); } } } @@ -99,7 +99,7 @@ class Client { logger.debug(`Received handshake: ${JSON.stringify(request)}`); if (request === undefined) { - return this.close({ error: 'Invalid handshake.', error_code: 0 }); + return this.close({error: 'Invalid handshake.', error_code: 0}); } let responded = false; @@ -107,7 +107,7 @@ class Client { const finish_handshake = () => { if (!responded) { responded = true; - const info = { token: res.token, id: res.payload.id, provider: res.payload.provider }; + const info = {token: res.token, id: res.payload.id, provider: res.payload.provider}; this.send_response(request, info); this._socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); @@ -136,7 +136,7 @@ class Client { }).catch((err) => { if (!responded) { responded = true; - this.close({ request_id: request.request_id, error: `${err}`, error_code: 0 }); + this.close({request_id: request.request_id, error: `${err}`, error_code: 0}); } }); } @@ -151,7 +151,7 @@ class Client { // there is no response for end_subscription return this.remove_request(raw_request); } else if (raw_request.type === 'keepalive') { - return this.send_response(raw_request, { state: 'complete' }); + return this.send_response(raw_request, {state: 'complete'}); } this._server.handle(raw_request); @@ -211,8 +211,8 @@ class Client { const error = err instanceof Error ? err.message : err; const error_code = code === undefined ? -1 : code; - this.send_response(request, { error, error_code }); + this.send_response(request, {error, error_code}); } } -module.exports = { Client }; +module.exports = {Client}; diff --git a/server/src/endpoint/insert.js b/server/src/endpoint/insert.js index 32e3863a4..04970f260 100644 --- a/server/src/endpoint/insert.js +++ b/server/src/endpoint/insert.js @@ -25,9 +25,9 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { (rows) => // write to database, all valid rows collection.table .insert(rows.map((row) => writes.apply_version(r.expr(row), 0)), - { returnChanges: 'always' }) + {returnChanges: 'always'}) .run(conn, reql_options) ).then(done).catch(done); }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/query.js b/server/src/endpoint/query.js index b6b8c9073..a02a6b742 100644 --- a/server/src/endpoint/query.js +++ b/server/src/endpoint/query.js @@ -10,7 +10,7 @@ const r = require('rethinkdb'); const object_to_fields = (obj) => Object.keys(obj).map((key) => { const value = obj[key]; - if (value !== null && typeof value === 'object' && !value['$reql_type$']) { + if (value !== null && typeof value === 'object' && !value.$reql_type$) { return object_to_fields(value).map((subkeys) => [ key ].concat(subkeys)); } else { return [ key ]; @@ -77,7 +77,7 @@ const make_reql = (raw_request, metadata) => { const order = (options.order && options.order[1] === 'descending') ? r.desc(index.name) : index.name; - return reql.orderBy({ index: order }).between(above_value, below_value, optargs); + return reql.orderBy({index: order}).between(above_value, below_value, optargs); }; if (options.find) { @@ -107,10 +107,10 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { done(new Error('Operation not permitted.')); cursor.close().catch(() => { }); } else { - send({ data: [ item ] }); + send({data: [ item ]}); } }).then(() => { - done({ data: [ ], state: 'complete' }); + done({data: [ ], state: 'complete'}); }); } else if (res !== null && res.constructor.name === 'Array') { for (const item of res) { @@ -118,11 +118,11 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { return done(new Error('Operation not permitted.')); } } - done({ data: res, state: 'complete' }); + done({data: res, state: 'complete'}); } else if (!ruleset.validate(context, res)) { done(new Error('Operation not permitted.')); } else { - done({ data: [ res ], state: 'complete' }); + done({data: [ res ], state: 'complete'}); } }).catch(done); @@ -133,4 +133,4 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { }; }; -module.exports = { make_reql, run }; +module.exports = {make_reql, run}; diff --git a/server/src/endpoint/remove.js b/server/src/endpoint/remove.js index fded2752b..372037df1 100644 --- a/server/src/endpoint/remove.js +++ b/server/src/endpoint/remove.js @@ -37,17 +37,17 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { // Otherwise, we can safely remove the row null), - { returnChanges: 'always' })) + {returnChanges: 'always'})) // Pretend like we deleted rows that didn't exist .do((res) => - res.merge({ changes: + res.merge({changes: r.range(row_data.count()).map((index) => r.branch(res('changes')(index)('old_val').eq(null), - res('changes')(index).merge({ old_val: { id: row_data(index)('id') } }), + res('changes')(index).merge({old_val: {id: row_data(index)('id')}}), res('changes')(index))).coerceTo('array'), }))) .run(conn, reql_options) ).then(done).catch(done); }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/replace.js b/server/src/endpoint/replace.js index 23b860bf3..666a55049 100644 --- a/server/src/endpoint/replace.js +++ b/server/src/endpoint/replace.js @@ -36,9 +36,9 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { // Otherwise, we can safely replace the row writes.apply_version(new_row, old_row(hz_v).default(-1).add(1))), - { returnChanges: 'always' })) + {returnChanges: 'always'})) .run(conn, reql_options) ).then(done).catch(done); }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/store.js b/server/src/endpoint/store.js index ad80d9a83..0fe287667 100644 --- a/server/src/endpoint/store.js +++ b/server/src/endpoint/store.js @@ -45,13 +45,13 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { // Otherwise, we can safely overwrite the row writes.apply_version(new_row, old_row(hz_v).default(-1).add(1)) ) - ), { returnChanges: 'always' }), + ), {returnChanges: 'always'}), // The new row does not have an id, so we insert it with an autogen id collection.table.insert(writes.apply_version(new_row, 0), - { returnChanges: 'always' }))) + {returnChanges: 'always'}))) .run(conn, reql_options) ).then(done).catch(done); }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/subscribe.js b/server/src/endpoint/subscribe.js index 280bcb334..f7af36422 100644 --- a/server/src/endpoint/subscribe.js +++ b/server/src/endpoint/subscribe.js @@ -7,11 +7,11 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { let feed; const reql = make_reql(raw_request, metadata); - reql.changes({ include_initial: true, + reql.changes({include_initial: true, include_states: true, include_types: true, include_offsets: Boolean(raw_request.options.order) && - Boolean(raw_request.options.limit) }) + Boolean(raw_request.options.limit)}) .run(metadata.connection(), reql_options) .then((res) => { feed = res; @@ -19,15 +19,15 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { if (item.state === 'initializing') { // Do nothing - we don't care } else if (item.state === 'ready') { - send({ state: 'synced' }); + send({state: 'synced'}); } else if ((item.old_val && !ruleset.validate(context, item.old_val)) || (item.new_val && !ruleset.validate(context, item.new_val))) { throw new Error('Operation not permitted.'); } else { - send({ data: [ item ] }); + send({data: [ item ]}); } }).then(() => { - done({ state: 'complete' }); + done({state: 'complete'}); }).catch(done); }).catch(done); @@ -38,4 +38,4 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { }; }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/update.js b/server/src/endpoint/update.js index 96bf68daf..bfe1d3e86 100644 --- a/server/src/endpoint/update.js +++ b/server/src/endpoint/update.js @@ -40,9 +40,9 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { // Otherwise we can safely update the row and increment the version writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1))), - { returnChanges: 'always' })) + {returnChanges: 'always'})) .run(conn, reql_options) ).then(done).catch(done); }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/upsert.js b/server/src/endpoint/upsert.js index 6f72142a3..69bea1065 100644 --- a/server/src/endpoint/upsert.js +++ b/server/src/endpoint/upsert.js @@ -51,13 +51,13 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { // Otherwise, we can safely update the row and increment the version writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1)) ) - ), { returnChanges: 'always' }), + ), {returnChanges: 'always'}), // The new row did not have an id, so we insert it with an autogen id collection.table.insert(writes.apply_version(new_row, 0), - { returnChanges: 'always' }))) + {returnChanges: 'always'}))) .run(conn, reql_options) ).then(done).catch(done); }; -module.exports = { run }; +module.exports = {run}; diff --git a/server/src/endpoint/writes.js b/server/src/endpoint/writes.js index e435c15b7..56b10087e 100644 --- a/server/src/endpoint/writes.js +++ b/server/src/endpoint/writes.js @@ -17,10 +17,10 @@ const apply_version = (row, new_version) => row.merge(r.object(hz_v, new_version const make_write_response = (data) => { data.forEach((item, index) => { if (item instanceof Error) { - data[index] = { error: item.message }; + data[index] = {error: item.message}; } }); - return { data, state: 'complete' }; + return {data, state: 'complete'}; }; // This function returns a Promise that resolves to an array of responses - one for each row in @@ -40,7 +40,8 @@ const make_write_response = (data) => { // rows: all pending rows // return: a (promise of a) ReQL write result object const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, do_write) => { - const iterate = (row_data, response_data, deadline_optional) => { + const iterate = (row_data_arg, response_data, deadline_optional) => { + let row_data = row_data_arg; let deadline = deadline_optional; if (row_data.length === 0) { return response_data; @@ -107,17 +108,17 @@ const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, const res = changes[index]; if (res.error !== undefined) { if (res.error.indexOf('Duplicate primary key') === 0) { - response_data[data.index] = { error: 'The document already exists.' }; + response_data[data.index] = {error: 'The document already exists.'}; } else if (res.error.indexOf(invalidated_msg) === 0 && data.version === undefined) { retry_rows.push(data); } else { - response_data[data.index] = { error: res.error }; + response_data[data.index] = {error: res.error}; } } else if (res.new_val === null) { - response_data[data.index] = { id: res.old_val.id, [hz_v]: res.old_val[hz_v] }; + response_data[data.index] = {id: res.old_val.id, [hz_v]: res.old_val[hz_v]}; } else { - response_data[data.index] = { id: res.new_val.id, [hz_v]: res.new_val[hz_v] }; + response_data[data.index] = {id: res.new_val.id, [hz_v]: res.new_val[hz_v]}; } }); @@ -126,7 +127,7 @@ const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, }); }; - return iterate(original_rows.map((row, index) => ({ row, index, version: row[hz_v] })), + return iterate(original_rows.map((row, index) => ({row, index, version: row[hz_v]})), Array(original_rows.length).fill(null), null).then(make_write_response); }; diff --git a/server/src/horizon.js b/server/src/horizon.js index e7e4e0b7b..90863e9e2 100644 --- a/server/src/horizon.js +++ b/server/src/horizon.js @@ -5,7 +5,7 @@ const joi = require('joi'); // Issue a dummy joi validation to force joi to initialize its scripts. // This is used because tests will mock the filesystem, and the lazy // `require`s done by joi will no longer work at that point. -joi.validate('', joi.any().when('', { is: '', then: joi.any() })); +joi.validate('', joi.any().when('', {is: '', then: joi.any()})); const server = require('./server'); diff --git a/server/src/metadata/collection.js b/server/src/metadata/collection.js index 6cc5ba760..422198741 100644 --- a/server/src/metadata/collection.js +++ b/server/src/metadata/collection.js @@ -88,4 +88,4 @@ class Collection { } } -module.exports = { Collection }; +module.exports = {Collection}; diff --git a/server/src/metadata/index.js b/server/src/metadata/index.js index e12c1fed5..4e81788d6 100644 --- a/server/src/metadata/index.js +++ b/server/src/metadata/index.js @@ -16,7 +16,7 @@ const primary_index_name = 'id'; const name_to_info = (name) => { if (name === primary_index_name) { - return { geo: false, multi: false, fields: [ 'id' ] }; + return {geo: false, multi: false, fields: [ 'id' ]}; } const re = /^hz_(?:(geo)_)?(?:multi_([0-9])+_)?\[/; @@ -26,7 +26,7 @@ const name_to_info = (name) => { const json_offset = matches[0].length - 1; - const info = { name, geo: Boolean(matches[1]), multi: isNaN(matches[2]) ? false : Number(matches[2]) }; + const info = {name, geo: Boolean(matches[1]), multi: isNaN(matches[2]) ? false : Number(matches[2])}; // Parse remainder as JSON try { @@ -189,4 +189,4 @@ class Index { } } -module.exports = { Index, primary_index_name, name_to_info, info_to_name, info_to_reql }; +module.exports = {Index, primary_index_name, name_to_info, info_to_name, info_to_reql}; diff --git a/server/src/metadata/metadata.js b/server/src/metadata/metadata.js deleted file mode 100644 index aaa279d1c..000000000 --- a/server/src/metadata/metadata.js +++ /dev/null @@ -1,374 +0,0 @@ -'use strict'; - -const error = require('../error'); -const logger = require('../logger'); -const Group = require('../permissions/group').Group; -const Collection = require('./collection').Collection; -const version_field = require('../endpoint/writes').version_field; -const utils = require('../utils'); - -const r = require('rethinkdb'); - -const metadata_version = [ 2, 0, 0 ]; - -const create_collection = (db, name, conn) => - r.db(db).table('hz_collections').get(name).replace({ id: name }).do((res) => - r.branch( - res('errors').ne(0), - r.error(res('first_error')), - res('inserted').eq(1), - r.db(db).tableCreate(name), - res - ) - ).run(conn); - -const initialize_metadata = (db, conn) => - r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) - .then(() => - Promise.all([ 'hz_collections', 'hz_users_auth', 'hz_groups' ].map((table) => - r.branch(r.db(db).tableList().contains(table), - { }, - r.db(db).tableCreate(table)) - .run(conn)))) - .then(() => - r.db(db).table('hz_collections').wait({ timeout: 30 }).run(conn)) - .then(() => - Promise.all([ - r.db(db).tableList().contains('users').not().run(conn).then(() => - create_collection(db, 'users', conn)), - r.db(db).table('hz_collections') - .insert({ id: 'hz_metadata', version: metadata_version }) - .run(conn), - ]) - ); - -class Metadata { - constructor(project_name, - reliable_conn, - clients, - auto_create_collection, - auto_create_index) { - this._db = project_name; - this._reliable_conn = reliable_conn; - this._clients = clients; - this._auto_create_collection = auto_create_collection; - this._auto_create_index = auto_create_index; - this._closed = false; - this._ready = false; - this._collections = new Map(); - this._groups = new Map(); - this._collection_feed = null; - this._group_feed = null; - this._index_feed = null; - - this._ready_promise = Promise.resolve().then(() => { - logger.debug('checking rethinkdb version'); - const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version') - return q.run(this._conn).then((res) => utils.rethinkdb_version_check(res)); - }).then(() => { - const old_metadata_db = `${this._db}_internal`; - return r.dbList().contains(old_metadata_db).run(this._conn).then((has_old_db) => { - if (has_old_db) { - throw new Error('The Horizon metadata appears to be from v1.x because ' + - `the "${old_metadata_db}" database exists. Please use ` + - '`hz migrate` to convert your metadata to the new format.'); - } - }); - }).then(() => { - logger.debug('checking for internal tables'); - if (this._auto_create_collection) { - return initialize_metadata(this._db, this._conn); - } else { - return r.dbList().contains(this._db).run(this._conn).then((has_db) => { - if (!has_db) { - throw new Error(`The database ${this._db} does not exist. ` + - 'Run `hz schema apply` to initialize the database, ' + - 'then start the Horizon server.'); - } - }); - } - }).then(() => { - logger.debug('waiting for internal tables'); - return r.expr([ 'hz_collections', 'hz_users_auth', 'hz_groups', 'users' ]) - .forEach((table) => r.db(this._db).table(table).wait({ timeout: 30 })).run(this._conn); - }).then(() => { - logger.debug('syncing metadata changefeeds'); - - const group_changefeed = - r.db(this._db) - .table('hz_groups') - .changes({ squash: true, - includeInitial: true, - includeStates: true, - includeTypes: true }) - .run(this._conn).then((res) => { - if (this._closed) { - res.close().catch(() => { }); - throw new Error('This metadata instance has been closed.'); - } - return new Promise((resolve, reject) => { - this._group_feed = res; - this._group_feed.eachAsync((change) => { - if (change.type === 'state') { - if (change.state === 'ready') { - logger.info('Groups metadata synced.'); - resolve(); - } - } else if (change.type === 'initial' || - change.type === 'add' || - change.type === 'change') { - const group = new Group(change.new_val); - this._groups.set(group.name, group); - this._clients.forEach((c) => c.group_changed(group.name)); - } else if (change.type === 'uninitial' || - change.type === 'remove') { - const group = this._groups.delete(change.old_val.id); - if (group) { - this._clients.forEach((c) => c.group_changed(group.name)); - } - } - }).catch(reject); - }); - }); - - const collection_changefeed = - r.db(this._db) - .table('hz_collections') - .filter((row) => row('id').match('^hz_').not()) - .changes({ squash: false, - includeInitial: true, - includeStates: true, - includeTypes: true }) - .run(this._conn).then((res) => { - if (this._closed) { - res.close().catch(() => { }); - throw new Error('This metadata instance has been closed.'); - } - return new Promise((resolve, reject) => { - this._collection_feed = res; - this._collection_feed.eachAsync((change) => { - if (change.type === 'state') { - if (change.state === 'ready') { - logger.info('Collections metadata synced.'); - resolve(); - } - } else if (change.type === 'initial' || - change.type === 'add' || - change.type === 'change') { - const collection_name = change.new_val.id; - let collection = this._collections.get(collection_name); - if (!collection) { - collection = new Collection(this._db, collection_name); - this._collections.set(collection_name, collection); - } - collection._register(); - } else if (change.type === 'uninitial' || - change.type === 'remove') { - const collection = this._collections.get(change.old_val.id); - if (collection) { - collection._unregister(); - if (collection._is_safe_to_remove()) { - this._collections.delete(change.old_val.id); - collection._close(); - } - } - } - }).catch(reject); - }); - }); - - const index_changefeed = - r.db('rethinkdb') - .table('table_config') - .filter((row) => r.and(row('db').eq(this._db), - row('name').match('^hz_').not())) - .map((row) => ({ - id: row('id'), - name: row('name'), - indexes: row('indexes').filter((idx) => idx.match('^hz_')), - })) - .changes({ squash: true, - includeInitial: true, - includeStates: true, - includeTypes: true }) - .run(this._conn).then((res) => { - if (this._closed) { - res.close().catch(() => { }); - throw new Error('This metadata instance has been closed.'); - } - return new Promise((resolve, reject) => { - this._index_feed = res; - this._index_feed.eachAsync((change) => { - if (change.type === 'state') { - if (change.state === 'ready') { - logger.info('Index metadata synced.'); - resolve(); - } - } else if (change.type === 'initial' || - change.type === 'add' || - change.type === 'change') { - const collection_name = change.new_val.name; - const table_id = change.new_val.id; - - let collection = this._collections.get(collection_name); - if (!collection) { - collection = new Collection(this._db, collection_name); - this._collections.set(collection_name, collection); - } - collection._update_table(table_id, change.new_val.indexes, this._conn); - } else if (change.type === 'uninitial' || - change.type === 'remove') { - const collection = this._collections.get(change.old_val.name); - if (collection) { - collection._update_table(change.old_val.id, null, this._conn); - if (collection._is_safe_to_remove()) { - this._collections.delete(collection); - collection._close(); - } - } - } - }).catch(reject); - }); - }); - - return Promise.all([ group_changefeed, collection_changefeed, index_changefeed ]); - }).then(() => { - logger.debug('adding admin user'); - // Ensure that the admin user and group exists - return Promise.all([ - r.db(this._db).table('users').get('admin') - .replace((old_row) => - r.branch(old_row.eq(null), - { - id: 'admin', - groups: [ 'admin' ], - [version_field]: 0, - }, - old_row), - { returnChanges: 'always' })('changes')(0) - .do((res) => - r.branch(res('new_val').eq(null), - r.error(res('error')), - res('new_val'))).run(this._conn), - r.db(this._db).table('hz_groups').get('admin') - .replace((old_row) => - r.branch(old_row.eq(null), - { - id: 'admin', - rules: { carte_blanche: { template: 'any()' } }, - [version_field]: 0, - }, - old_row), - { returnChanges: 'always' })('changes')(0) - .do((res) => - r.branch(res('new_val').eq(null), - r.error(res('error')), - res('new_val'))).run(this._conn), - ]); - }).then(() => { - logger.debug('metadata sync complete'); - this._ready = true; - return this; - }); - - this._ready_promise.catch(() => { - this.close(); - }); - } - - close() { - this._closed = true; - this._ready = false; - - if (this._group_feed) { - this._group_feed.close().catch(() => { }); - } - if (this._collection_feed) { - this._collection_feed.close().catch(() => { }); - } - if (this._index_feed) { - this._index_feed.close().catch(() => { }); - } - - this._collections.forEach((collection) => collection._close()); - this._collections.clear(); - } - - is_ready() { - return this._ready; - } - - ready() { - return this._ready_promise; - } - - collection(name) { - if (name.indexOf('hz_') === 0) { - throw new Error(`Collection "${name}" is reserved for internal use ` + - 'and cannot be used in requests.'); - } - - const collection = this._collections.get(name); - if (collection === undefined) { throw new error.CollectionMissing(name); } - if (!collection._get_table().ready()) { throw new error.CollectionNotReady(collection); } - return collection; - } - - handle_error(err, done) { - logger.debug(`Handling error: ${err.message}`); - try { - if (err instanceof error.CollectionNotReady) { - return err.collection._on_ready(done); - } else if (err instanceof error.IndexNotReady) { - return err.index.on_ready(done); - } else if (this._auto_create_collection && (err instanceof error.CollectionMissing)) { - logger.warn(`Auto-creating collection: ${err.name}`); - return this.create_collection(err.name, done); - } else if (this._auto_create_index && (err instanceof error.IndexMissing)) { - logger.warn(`Auto-creating index on collection "${err.collection.name}": ` + - `${JSON.stringify(err.fields)}`); - return err.collection._create_index(err.fields, this._conn, done); - } - done(err); - } catch (new_err) { - logger.debug(`Error when handling error: ${new_err.message}`); - done(new_err); - } - } - - create_collection(name, done) { - error.check(this._collections.get(name) === undefined, - `Collection "${name}" already exists.`); - - const collection = new Collection(this._db, name); - this._collections.set(name, collection); - - create_collection(this._db, name, this._conn).then((res) => { - error.check(!res.error, `Collection "${name}" creation failed: ${res.error}`); - logger.warn(`Collection created: "${name}"`); - collection._on_ready(done); - }).catch((err) => { - if (collection._is_safe_to_remove()) { - this._collections.delete(name); - collection._close(); - } - done(err); - }); - } - - get_user_feed(id) { - return r.db(this._db).table('users').get(id) - .changes({ includeInitial: true, squash: true }) - .run(this._conn); - } - - get_group(group_name) { - return this._groups.get(group_name); - } - - connection() { - return this._conn; - } -} - -module.exports = { Metadata, create_collection, initialize_metadata }; diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index d01101aba..ba7364de5 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -1,6 +1,8 @@ 'use strict'; const error = require('../error'); +const version_field = require('../endpoints/writes').version_field; +const {Reliable, ReliableChangefeed, ReliableUnion} = require('../reliable'); const logger = require('../logger'); const Collection = require('./collection').Collection; const utils = require('../utils'); @@ -10,7 +12,7 @@ const r = require('rethinkdb'); const metadata_version = [ 2, 0, 0 ]; const create_collection = (db, name, conn) => - r.db(db).table('hz_collections').get(name).replace({ id: name }).do((res) => + r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => r.branch( res('errors').ne(0), r.error(res('first_error')), @@ -29,13 +31,13 @@ const initialize_metadata = (db, conn) => r.db(db).tableCreate(table)) .run(conn)))) .then(() => - r.db(db).table('hz_collections').wait({ timeout: 30 }).run(conn)) + r.db(db).table('hz_collections').wait({timeout: 30}).run(conn)) .then(() => Promise.all([ r.db(db).tableList().contains('users').not().run(conn).then(() => create_collection(db, 'users', conn)), r.db(db).table('hz_collections') - .insert({ id: 'hz_metadata', version: metadata_version }) + .insert({id: 'hz_metadata', version: metadata_version}) .run(conn), ]) ); @@ -50,7 +52,7 @@ class ReliableInit extends Reliable { this._conn_subs = reliable_conn.subscribe({ onReady: (conn) => { this.current_attempt = Symbol(); - do_init(conn, this.current_attempt); + this.do_init(conn, this.current_attempt); }, onUnready: () => { this.current_attempt = null; @@ -71,7 +73,7 @@ class ReliableInit extends Reliable { Promise.resolve().then(() => { this.check_attempt(attempt); logger.debug('checking rethinkdb version'); - const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version') + const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version'); return q.run(conn).then((res) => utils.rethinkdb_version_check(res)); }).then(() => { this.check_attempt(attempt); @@ -102,7 +104,7 @@ class ReliableInit extends Reliable { this.check_attempt(attempt); logger.debug('waiting for internal tables'); return r.expr([ 'hz_collections', 'hz_users_auth', 'hz_groups', 'users' ]) - .forEach((table) => r.db(this._db).table(table).wait({ timeout: 30 })).run(conn); + .forEach((table) => r.db(this._db).table(table).wait({timeout: 30})).run(conn); }).then(() => { this.check_attempt(attempt); logger.debug('adding admin user'); @@ -116,7 +118,7 @@ class ReliableInit extends Reliable { [version_field]: 0, }, old_row), - { returnChanges: 'always' })('changes')(0) + {returnChanges: 'always'})('changes')(0) .do((res) => r.branch(res('new_val').eq(null), r.error(res('error')), @@ -126,11 +128,11 @@ class ReliableInit extends Reliable { r.branch(old_row.eq(null), { id: 'admin', - rules: { carte_blanche: { template: 'any()' } }, + rules: {carte_blanche: {template: 'any()'}}, [version_field]: 0, }, old_row), - { returnChanges: 'always' })('changes')(0) + {returnChanges: 'always'})('changes')(0) .do((res) => r.branch(res('new_val').eq(null), r.error(res('error')), @@ -164,6 +166,8 @@ class ReliableMetadata extends Reliable { this._db = project_name; this._reliable_conn = reliable_conn; + this._auto_create_collection = auto_create_collection; + this._auto_create_index = auto_create_index; this._collections = new Map(); this._reliable_init = new ReliableInit(reliable_conn); @@ -172,7 +176,7 @@ class ReliableMetadata extends Reliable { r.db(this._db) .table('hz_collections') .filter((row) => row('id').match('^hzp?_').not()) - .changes({ squash: false, includeInitial: true, includeTypes: true }), + .changes({squash: false, includeInitial: true, includeTypes: true}), reliable_conn, { onChange: (change) => { @@ -210,7 +214,7 @@ class ReliableMetadata extends Reliable { } }, }); - + this._index_changefeed = new ReliableChangefeed( r.db('rethinkdb') @@ -222,14 +226,7 @@ class ReliableMetadata extends Reliable { name: row('name'), indexes: row('indexes').filter((idx) => idx.match('^hz_')), })) - .changes({ squash: true, - includeInitial: true, - includeStates: true, - includeTypes: true }) - r.db(this._db) - .table('hz_collections') - .filter((row) => row('id').match('^hzp?_').not()) - .changes({ squash: false, includeInitial: true, includeTypes: true }), + .changes({squash: true, includeInitial: true, includeTypes: true}), reliable_conn, { onChange: (change) => { @@ -278,7 +275,8 @@ class ReliableMetadata extends Reliable { this.emit('onReady'); }, onUnready: () => { - this._collections.forEach((collection) => collection.close(reason)); + // TODO: fill in the reason for `close`. + this._collections.forEach((collection) => collection.close()); this._collections.clear(); }, }); @@ -361,4 +359,4 @@ class ReliableMetadata extends Reliable { } } -module.exports = { ReliableMetadata, create_collection, initialize_metadata }; +module.exports = {ReliableMetadata, create_collection, initialize_metadata}; diff --git a/server/src/metadata/table.js b/server/src/metadata/table.js index 828310ddd..3945ffc40 100644 --- a/server/src/metadata/table.js +++ b/server/src/metadata/table.js @@ -15,7 +15,7 @@ class Table { this._result = null; this.table - .wait({ waitFor: 'all_replicas_ready' }) + .wait({waitFor: 'all_replicas_ready'}) .run(conn) .then(() => { this._result = true; @@ -79,7 +79,7 @@ class Table { // TODO: support geo and multi indexes create_index(fields, conn, done) { - const info = { geo: false, multi: false, fields }; + const info = {geo: false, multi: false, fields}; const index_name = index.info_to_name(info); error.check(!this.indexes.get(index_name), 'index already exists'); @@ -92,7 +92,7 @@ class Table { }; this.table.indexCreate(index_name, index.info_to_reql(info), - { geo: info.geo, multi: (info.multi !== false) }) + {geo: info.geo, multi: (info.multi !== false)}) .run(conn) .then(success) .catch((err) => { @@ -127,4 +127,4 @@ class Table { } } -module.exports = { Table }; +module.exports = {Table}; diff --git a/server/src/permissions/group.js b/server/src/permissions/group.js index f10d34602..9b404c2ca 100644 --- a/server/src/permissions/group.js +++ b/server/src/permissions/group.js @@ -8,4 +8,4 @@ class Group { } } -module.exports = { Group }; +module.exports = {Group}; diff --git a/server/src/permissions/rule.js b/server/src/permissions/rule.js index eba453710..6d7cd29a3 100644 --- a/server/src/permissions/rule.js +++ b/server/src/permissions/rule.js @@ -63,6 +63,6 @@ class Ruleset { } // The any_rule is used when permissions are disabled - it allows all queries -const any_rule = new Rule('permissions_disabled', { template: 'any()' }); +const any_rule = new Rule('permissions_disabled', {template: 'any()'}); -module.exports = { Rule, Ruleset, any_rule }; +module.exports = {Rule, Ruleset, any_rule}; diff --git a/server/src/permissions/template.js b/server/src/permissions/template.js index ab0b8bf4b..cf93595ca 100644 --- a/server/src/permissions/template.js +++ b/server/src/permissions/template.js @@ -96,7 +96,7 @@ const wrap_write = (query, docs) => { const wrap_remove = (doc) => { if (validIndexValue(doc)) { - return { id: doc }; + return {id: doc}; } return doc; }; @@ -148,9 +148,9 @@ ast.Collection.prototype.removeAll = function(docs) { const env = { collection: (name) => new ast.Collection((type, options) => - ({ request_id: new Any(), + ({request_id: new Any(), type: Array.isArray(type) ? new Any(type) : type, - options }), name, false), + options}), name, false), any: function() { return new Any(Array.from(arguments)); }, anyObject: function(obj) { return new AnyObject(obj); }, anyArray: function() { return new AnyArray(Array.from(arguments)); }, @@ -246,4 +246,4 @@ class Template { } } -module.exports = { Template }; +module.exports = {Template}; diff --git a/server/src/permissions/validator.js b/server/src/permissions/validator.js index 1e5280c19..f3a3cb152 100644 --- a/server/src/permissions/validator.js +++ b/server/src/permissions/validator.js @@ -29,4 +29,4 @@ class Validator { } -module.exports = { Validator }; +module.exports = {Validator}; diff --git a/server/src/reql_connection.js b/server/src/reql_connection.js index 697b70b22..7e1494f08 100644 --- a/server/src/reql_connection.js +++ b/server/src/reql_connection.js @@ -34,7 +34,7 @@ class ReqlConnection { } this._clients.forEach((client) => - client.close({ error: err.message })); + client.close({error: err.message})); this._clients.clear(); this._interrupted_err = err; @@ -58,7 +58,7 @@ class ReqlConnection { this._metadata = null; this._clients.forEach((client) => - client.close({ error: 'Connection to the database was lost.' })); + client.close({error: 'Connection to the database was lost.'})); this._clients.clear(); if (this._interrupted_err) { @@ -127,4 +127,4 @@ class ReqlConnection { } } -module.exports = { ReqlConnection }; +module.exports = {ReqlConnection}; diff --git a/server/src/response.js b/server/src/response.js index 73627adab..e35b13c6d 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -128,4 +128,4 @@ class Request { } } -module.exports = { Request }; +module.exports = {Request}; diff --git a/server/src/schema/horizon_protocol.js b/server/src/schema/horizon_protocol.js index 6a4c5323e..8ec4b5228 100644 --- a/server/src/schema/horizon_protocol.js +++ b/server/src/schema/horizon_protocol.js @@ -6,7 +6,7 @@ const handshake = Joi.object().keys({ request_id: Joi.number().required(), method: Joi.only('token', 'anonymous', 'unauthenticated').required(), token: Joi.string().required() - .when('method', { is: Joi.not('token').required(), then: Joi.forbidden() }), + .when('method', {is: Joi.not('token').required(), then: Joi.forbidden()}), }).unknown(false); const read = Joi.alternatives().try( @@ -18,22 +18,22 @@ const read = Joi.alternatives().try( collection: Joi.string().token().required(), limit: Joi.number().integer().greater(-1).optional() - .when('find', { is: Joi.any().required(), then: Joi.forbidden() }), + .when('find', {is: Joi.any().required(), then: Joi.forbidden()}), order: Joi.array().ordered( Joi.array().items(Joi.string()).min(1).unique().label('fields').required(), Joi.string().valid('ascending', 'descending').label('direction').required()).optional() - .when('find_all', { is: Joi.array().min(2).required(), then: Joi.forbidden() }), + .when('find_all', {is: Joi.array().min(2).required(), then: Joi.forbidden()}), above: Joi.array().ordered( Joi.object().length(1).unknown(true).label('value').required(), Joi.string().valid('open', 'closed').label('bound_type').required()).optional() - .when('find_all', { is: Joi.array().min(2).required(), then: Joi.forbidden() }), + .when('find_all', {is: Joi.array().min(2).required(), then: Joi.forbidden()}), below: Joi.array().ordered( Joi.object().length(1).unknown(true).label('value').required(), Joi.string().valid('open', 'closed').label('bound_type').required()).optional() - .when('find_all', { is: Joi.array().min(2).required(), then: Joi.forbidden() }), + .when('find_all', {is: Joi.array().min(2).required(), then: Joi.forbidden()}), find_all: Joi.array().items(Joi.object().min(1).label('item').unknown(true)).min(1).optional(), }).unknown(false) @@ -59,7 +59,7 @@ const request = Joi.object({ request_id: Joi.number().required(), type: Joi.string().required(), options: Joi.object().required() - .when('type', { is: Joi.string().only('end_subscription'), then: Joi.forbidden() }), + .when('type', {is: Joi.string().only('end_subscription'), then: Joi.forbidden()}), }).unknown(false); module.exports = { diff --git a/server/src/schema/server_options.js b/server/src/schema/server_options.js index bea30bece..8844ca20d 100644 --- a/server/src/schema/server_options.js +++ b/server/src/schema/server_options.js @@ -36,4 +36,4 @@ const auth = Joi.object({ allow_unauthenticated: Joi.boolean().default(false), }).unknown(false); -module.exports = { server, auth }; +module.exports = {server, auth}; From a35437d6657bb5a2b364dd4fd737b3df6cc5b85a Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Tue, 16 Aug 2016 03:32:46 +0000 Subject: [PATCH 10/86] style fixes --- server/src/permissions/template.js | 6 ++-- server/src/reliable.js | 55 ++++++++++++++++++------------ server/src/response.js | 4 +-- server/src/server.js | 43 +++++++++-------------- 4 files changed, 53 insertions(+), 55 deletions(-) diff --git a/server/src/permissions/template.js b/server/src/permissions/template.js index cf93595ca..0bb73e6db 100644 --- a/server/src/permissions/template.js +++ b/server/src/permissions/template.js @@ -7,8 +7,6 @@ const ast = require('@horizon/client/lib/ast'); const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; const vm = require('vm'); -let template_compare; - class Any { constructor(values) { this._values = values || [ ]; @@ -167,7 +165,7 @@ const make_template = (str) => { }; // eslint-disable-next-line prefer-const -template_compare = (query, template, context) => { +function template_compare(query, template, context) { if (template === undefined) { return false; } else if (template instanceof Any || @@ -216,7 +214,7 @@ template_compare = (query, template, context) => { } return true; -}; +} const incomplete_template_message = (str) => `Incomplete template "${str}", ` + diff --git a/server/src/reliable.js b/server/src/reliable.js index 416ac1ae8..91989a4a0 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -1,5 +1,9 @@ 'use strict'; +const logger = require('./logger'); + +const r = require('rethinkdb'); + class Reliable { constructor(initialCbs) { this.subs = {}; @@ -12,10 +16,10 @@ class Reliable { subscribe(cbs) { if (this.closed) { - throw new Error("Cannot subscribe to a closed ReliableConn."); + throw new Error('Cannot subscribe to a closed ReliableConn.'); } const subId = Symbol(); - subs[subId] = { + this.subs[subId] = { cbs: cbs, close: () => delete this.subs[subId], }; @@ -26,7 +30,7 @@ class Reliable { // log e } } - return subs[subId]; + return this.subs[subId]; } emit() { @@ -40,12 +44,12 @@ class Reliable { // TODO: consider checking to make sure we don't send two // `onReady` or `onUnready`s in a row (or even just returning // early if we would). - if (eventType == "onReady") { + if (eventType === 'onReady') { this.ready = arguments; - } else if (eventType == "onUnready") { + } else if (eventType === 'onUnready') { this.ready = false; } - for (let s of Object.getOwnPropertySymbols(this.subs)) { + for (const s of Object.getOwnPropertySymbols(this.subs)) { try { const cbs = this.subs[s].cbs; const event = cbs[eventType]; @@ -66,7 +70,7 @@ class Reliable { } } -class ReliableConn extends Reliable{ +class ReliableConn extends Reliable { constructor(connOpts) { super(); this.connOpts = connOpts; @@ -85,24 +89,24 @@ class ReliableConn extends Reliable{ this.connect(); } } - }) + }); } else { conn.close(); } }).catch((e) => { if (this.conn) { - // RSI: log a scary error. + logger.error(`Error in ${JSON.stringify(this)}: ${e.stack}`); } if (!this.closed) { setTimeout(1000, () => this.connect()); } - }) + }); } close(reason) { let retProm = super.close(reason); if (this.conn) { - retProm = Promise.all([retProm, this.conn.close()]); + retProm = Promise.all([ retProm, this.conn.close() ]); } return retProm; } @@ -119,7 +123,7 @@ class ReliableCfeed extends Reliable { onReady: (conn) => { reql.run(conn, {includeTypes: true, includeStates: true}).then((cursor) => { this.cursor = cursor; - return cursor.eachAsync(change) { + return cursor.eachAsync((change) => { switch (change.type) { case 'state': if (change.state === 'ready') { @@ -129,21 +133,21 @@ class ReliableCfeed extends Reliable { default: this.emit('onChange', change); } - } - }).then((res) => { - // If we get here the cursor closed for some reason. - throw new Error("cursor closed for some reason"); - }).catch((e) => { - this.emit('onUnready', e); + }).then((res) => { + // If we get here the cursor closed for some reason. + throw new Error(`cursor closed for some reason: ${res}`); + }).catch((e) => { + this.emit('onUnready', e); + }); }); - } + }, }); } close(reason) { let retProm = super.close(reason); if (this.cursor) { - retProm = Promise.all([retProm, this.cursor.close()]); + retProm = Promise.all([ retProm, this.cursor.close() ]); } this.subscription.close(); return retProm; @@ -176,9 +180,9 @@ class ReliableUnion extends Reliable { maybeEmit() { if (this.readyNeeded === 0 && !this.ready) { - this.emit('onReady', emitArg); + this.emit('onReady', this.emitArg); } else if (this.readyNeeded !== 0 && this.ready) { - this.emit('onUnready', emitArg); + this.emit('onUnready', this.emitArg); } } @@ -189,3 +193,10 @@ class ReliableUnion extends Reliable { return super.close(reason); } } + +module.exports = { + Reliable, + ReliableConn, + ReliableCfeed, + ReliableUnion, +}; diff --git a/server/src/response.js b/server/src/response.js index e35b13c6d..e71f91624 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -12,7 +12,7 @@ class Response { this._reject = reject; }).then((data) => { this._completed = true; - write(data, 'complete'); + this.write(data, 'complete'); }).catch((err) => { this._completed = true; this._socketSend({ @@ -128,4 +128,4 @@ class Request { } } -module.exports = {Request}; +module.exports = {Response, Request}; diff --git a/server/src/server.js b/server/src/server.js index 334ff24e2..5f33c7806 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -2,20 +2,13 @@ const Auth = require('./auth').Auth; const Client = require('./client').Client; -const ReqlConnection = require('./reql_connection').ReqlConnection; const logger = require('./logger'); +const {ReliableConn} = require('./reliable'); +const {ReliableMetadata} = require('./metadata/reliable_metadata'); const options_schema = require('./schema/server_options').server; -const getType = require('mime-types').contentType; -// TODO: dynamically serve different versions of the horizon -// library. Minified, Rx included etc. -const horizon_client_path = require.resolve('@horizon/client/dist/horizon'); - -const assert = require('assert'); -const fs = require('fs'); +const EventEmitter = require('events'); const Joi = require('joi'); -const path = require('path'); -const url = require('url'); const websocket = require('ws'); const protocol_name = 'rethinkdb-horizon-v0'; @@ -27,10 +20,11 @@ function handleProtocols(protocols, cb) { logger.debug(`Rejecting client without "${protocol_name}" protocol (${protocols}).`); cb(false, null); } -}; +} class Server extends EventEmitter { constructor(http_servers, user_opts) { + super(); const opts = Joi.attempt(user_opts || { }, options_schema); this._original_user_opts = user_opts; this._auth_methods = { }; @@ -58,19 +52,16 @@ class Server extends EventEmitter { this._clear_clients_subscription = this._reliable_metadata.subscribe({ onReady: () => { this.emit('ready', this); - } + }, onUnready: (err) => { this.emit('unready', this); const msg = (err && err.message) || 'Connection became unready.'; - this._clients.forEach((client) => client.close({ error: msg })); + this._clients.forEach((client) => client.close({error: msg})); this._clients.clear(); - } + }, }); this._auth = new Auth(this, opts.auth); - for (const key in endpoints) { - this.add_request_handler(key, endpoints[key].run); - } const verifyClient = (info, cb) => { // Reject connections if we aren't synced with the database @@ -81,11 +72,11 @@ class Server extends EventEmitter { } }; - const ws_options = { handleProtocols, verifyClient, path: opts.path }; + const ws_options = {handleProtocols, verifyClient, path: opts.path}; // RSI: only become ready when this and metadata are both ready. const add_websocket = (server) => { - const ws_server = new websocket.Server(Object.assign({ server }, ws_options)) + const ws_server = new websocket.Server(Object.assign({server}, ws_options)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => new Client(socket, this)); @@ -115,20 +106,18 @@ class Server extends EventEmitter { // them to be closed. close() { if (!this._close_promise) { - this._close_promise = this._reliable_metadata.close().then(() => { - return Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { + this._close_promise = this._reliable_metadata.close().then( + () => Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { s.close(resolve); - }))).then(() => { - return this._reliable_conn.close(); - }); - }); + }))) + ).then( + () => this._reliable_conn.close() + ); } return this._close_promise; } } - - module.exports = { Server, protocol: protocol_name, From 365896fec603cc4b07d7146614f83a8aa38dfa91 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Tue, 16 Aug 2016 04:30:53 +0000 Subject: [PATCH 11/86] checkpoint --- server/package.json | 9 ++++--- server/src/client.js | 55 ++++++++++++++++++------------------------ server/src/response.js | 7 +++--- server/src/server.js | 10 +++++++- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/server/package.json b/server/package.json index ecbb11580..3fd5f2e92 100644 --- a/server/package.json +++ b/server/package.json @@ -2,11 +2,12 @@ "name": "@horizon/server", "version": "2.0.0-beta-5", "description": "Server for RethinkDB Horizon, an open-source developer platform for building realtime, scalable web apps.", - "main": "src/horizon.js", + "main": "dist/horizon.js", "scripts": { "lint": "eslint src test", "test": "mocha test/test.js test/schema.js test/permissions.js --timeout 10000", - "coverage": "istanbul cover _mocha test/test.js" + "coverage": "istanbul cover _mocha test/test.js", + "build": "babel src -d dist -s true" }, "repository": { "type": "git", @@ -38,6 +39,8 @@ "eslint": "^2.3.0", "istanbul": "^0.4.3", "mocha": "^2.3.3", - "nodemon": "^1.8.1" + "nodemon": "^1.8.1", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" } } diff --git a/server/src/client.js b/server/src/client.js index abbcc5efa..b5120b944 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -2,24 +2,21 @@ const logger = require('./logger'); const schemas = require('./schema/horizon_protocol'); -const Request = require('./request').Request; +const Response = require('./response').Response; const Joi = require('joi'); const websocket = require('ws'); class Client { - constructor(socket, server) { + constructor(socket, reliable_metadata, middleware_cb) { logger.debug('Client connection established.'); this._socket = socket; - this._server = server; + this._reliable_metadata = reliable_metadata; + this._middleware_cb = middleware_cb; this._auth = this._server._auth; - this._permissions_enabled = this._server._permissions_enabled; - this._metadata = this._server._reql_conn.metadata(); this._requests = new Map(); this.user_info = { }; - this._server._reql_conn._clients.add(this); - this._socket.on('close', (code, msg) => this.handle_websocket_close(code, msg)); @@ -107,8 +104,12 @@ class Client { const finish_handshake = () => { if (!responded) { responded = true; - const info = {token: res.token, id: res.payload.id, provider: res.payload.provider}; - this.send_response(request, info); + const info = { + token: res.token, + id: res.payload.id, + provider: res.payload.provider, + }; + this.send_message(request, info); this._socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); } @@ -149,32 +150,22 @@ class Client { return; } else if (raw_request.type === 'end_subscription') { // there is no response for end_subscription - return this.remove_request(raw_request); + return this.remove_response(raw_request.request_id); } else if (raw_request.type === 'keepalive') { - return this.send_response(raw_request, {state: 'complete'}); - } - - this._server.handle(raw_request); - - const endpoint = this._server.get_request_handler(raw_request); - if (endpoint === undefined) { - return this.send_error(raw_request, - `"${raw_request.type}" is not a registered request type.`); - } else if (this._requests.has(raw_request.request_id)) { - return this.send_error(raw_request, - `Request ${raw_request.request_id} already exists for this client.`); + return this.send_message(raw_request, {state: 'complete'}); } - const request = new Request(raw_request, endpoint, this); - this._requests.set(raw_request.request_id, request); - request.run(); + const resp = new Response((obj) => this.send_message(raw_request, obj)); + this.responses.set(raw_request.request_id, resp); + this._middleware_cb(raw_request, resp); } - remove_request(raw_request) { - const request = this._requests.get(raw_request.request_id); - this._requests.delete(raw_request.request_id); - if (request) { - request.close(); + + remove_response(request_id) { + const response = this._responses.get(request_id); + this._responses.delete(request_id); + if (response) { + response.close(); } } @@ -196,7 +187,7 @@ class Client { } } - send_response(request, data) { + send_message(request, data) { // Ignore responses for disconnected clients if (this.is_open()) { data.request_id = request.request_id; @@ -211,7 +202,7 @@ class Client { const error = err instanceof Error ? err.message : err; const error_code = code === undefined ? -1 : code; - this.send_response(request, {error, error_code}); + this.send_message(request, {error, error_code}); } } diff --git a/server/src/response.js b/server/src/response.js index e71f91624..46de0c3f2 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -4,8 +4,8 @@ const logger = require('./logger'); const rule = require('./permissions/rule'); class Response { - constructor(requestId, socketSend) { - this._requestId = requestId; + // RSI: make sure socketSend sets `request_id`. + constructor(socketSend) { this._socketSend = socketSend; this.complete = new Promise((resolve, reject) => { this._resolve = resolve; @@ -16,7 +16,6 @@ class Response { }).catch((err) => { this._completed = true; this._socketSend({ - request_id: this._requestId, error: `${err}`, error_code: err.code || -1, }); @@ -32,7 +31,7 @@ class Response { '`.write()` cannot be used to send a `state: complete` message.' + ' Use `.end()` to complete a Response.'); } - this._socketSend({state, data, request_id: this._requestId}); + this._socketSend({state, data}); } end(dataOrError) { diff --git a/server/src/server.js b/server/src/server.js index 5f33c7806..2dd739f0c 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -78,7 +78,15 @@ class Server extends EventEmitter { const add_websocket = (server) => { const ws_server = new websocket.Server(Object.assign({server}, ws_options)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) - .on('connection', (socket) => new Client(socket, this)); + .on('connection', (socket) => { + try { + const client = new Client(socket, this, () => ...); + this._clients.add(client); + socket.on('close', () => this._clients.delete(client)); + } catch (e) { + // RSI: do something. + } + }); this._ws_servers.push(ws_server); }; From 58cc7f35cad78531e4e8126d67e60bac18e9763b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 16 Aug 2016 01:01:00 -0700 Subject: [PATCH 12/86] fixed syntax --- plugin_router/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 8ea1fe55e..9ccad83d3 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -73,7 +73,7 @@ class PluginRouter { } hzMiddleware() { - return (req, res, next) { + return (req, res, next) => { const method = (req.type && this.methods[req.type]) || next; let cb = method; if (req.options) { From b10f02c4d2b31ee063a09d4ef2179dc782ae3c68 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 16 Aug 2016 16:05:14 -0700 Subject: [PATCH 13/86] working on middleware execution --- server/src/client.js | 71 +++++++++++++++++++++++--------------------- server/src/server.js | 36 ++++++++++++++++++++-- 2 files changed, 70 insertions(+), 37 deletions(-) diff --git a/server/src/client.js b/server/src/client.js index b5120b944..15597635d 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -8,40 +8,41 @@ const Joi = require('joi'); const websocket = require('ws'); class Client { - constructor(socket, reliable_metadata, middleware_cb) { + constructor(socket, auth, reliable_metadata, + auth_middleware_cb, request_middleware_cb) { logger.debug('Client connection established.'); - this._socket = socket; - this._reliable_metadata = reliable_metadata; - this._middleware_cb = middleware_cb; - this._auth = this._server._auth; - this._requests = new Map(); + this.socket = socket; + this.auth = auth; + this.reliable_metadata = reliable_metadata; + this.auth_middleware_cb = auth_middleware_cb; + this.request_middleware_cb = request_middleware_cb; + + this.responses = new Map(); this.user_info = { }; - this._socket.on('close', (code, msg) => + this.socket.on('close', (code, msg) => this.handle_websocket_close(code, msg)); - this._socket.on('error', (error) => + this.socket.on('error', (error) => this.handle_websocket_error(error)); // The first message should always be the handshake - this._socket.once('message', (data) => + this.socket.once('message', (data) => this.error_wrap_socket(() => this.handle_handshake(data))); - if (!this._metadata.is_ready()) { - this.close({error: 'No connection to the database.'}); + if (!this.metadata.ready) { + throw new Error('No connection to the database.'); } } handle_websocket_close() { logger.debug('Client connection terminated.'); + // RSI: move to permissions? if (this.user_feed) { this.user_feed.close().catch(() => { }); } - this._requests.forEach((request) => { - request.close(); - }); - this._requests.clear(); - this._server._reql_conn._clients.delete(this); + this.responses.forEach((response) => response.close()); + this.responses.clear(); } handle_websocket_error(code, msg) { @@ -54,8 +55,8 @@ class Client { } catch (err) { logger.debug(`Unhandled error in request: ${err.stack}`); this.close({request_id: null, - error: `Unhandled error: ${err}`, - error_code: 0}); + error: `Unhandled error: ${err}`, + error_code: 0}); } } @@ -65,8 +66,8 @@ class Client { request = JSON.parse(data); } catch (err) { return this.close({request_id: null, - error: `Invalid JSON: ${err}`, - error_code: 0}); + error: `Invalid JSON: ${err}`, + error_code: 0}); } try { @@ -85,9 +86,10 @@ class Client { } } + // RSI: move to permissions plugin group_changed(group_name) { if (this.user_info.groups.indexOf(group_name) !== -1) { - this._requests.forEach((req) => req.evaluate_rules()); + this.responses.forEach((response) => response.evaluate_rules()); } } @@ -100,7 +102,7 @@ class Client { } let responded = false; - this._server._auth.handshake(request).then((res) => { + this.auth.handshake(request).then((res) => { const finish_handshake = () => { if (!responded) { responded = true; @@ -110,21 +112,22 @@ class Client { provider: res.payload.provider, }; this.send_message(request, info); - this._socket.on('message', (msg) => + this.socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); } }; this.user_info = res.payload; if (this.user_info.id != null) { - return this._metadata.get_user_feed(this.user_info.id).then((feed) => { + // RSI: move to permissions plugin + return this.metadata.get_user_feed(this.user_info.id).then((feed) => { this.user_feed = feed; return feed.eachAsync((change) => { if (!change.new_val) { throw new Error('User account has been deleted.'); } Object.assign(this.user_info, change.new_val); - this._requests.forEach((req) => req.evaluate_rules()); + this.responses.forEach((response) => response.evaluate_rules()); finish_handshake(); }).then(() => { throw new Error('User account feed has been lost.'); @@ -155,22 +158,22 @@ class Client { return this.send_message(raw_request, {state: 'complete'}); } - const resp = new Response((obj) => this.send_message(raw_request, obj)); - this.responses.set(raw_request.request_id, resp); - this._middleware_cb(raw_request, resp); + const response = new Response((obj) => this.send_message(raw_request, obj)); + this.responses.set(raw_request.request_id, response); + this.request_middleware_cb(raw_request, response); } remove_response(request_id) { - const response = this._responses.get(request_id); - this._responses.delete(request_id); + const response = this.responses.get(request_id); + this.responses.delete(request_id); if (response) { response.close(); } } is_open() { - return this._socket.readyState === websocket.OPEN; + return this.socket.readyState === websocket.OPEN; } close(info) { @@ -181,9 +184,9 @@ class Client { `${info.error || 'Unspecified reason.'}`); logger.debug(`info: ${JSON.stringify(info)}`); if (info.request_id !== undefined) { - this._socket.send(JSON.stringify(info)); + this.socket.send(JSON.stringify(info)); } - this._socket.close(1002, close_msg); + this.socket.close(1002, close_msg); } } @@ -192,7 +195,7 @@ class Client { if (this.is_open()) { data.request_id = request.request_id; logger.debug(`Sending response: ${JSON.stringify(data)}`); - this._socket.send(JSON.stringify(data)); + this.socket.send(JSON.stringify(data)); } } diff --git a/server/src/server.js b/server/src/server.js index 2dd739f0c..f582a9b5e 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -74,17 +74,47 @@ class Server extends EventEmitter { const ws_options = {handleProtocols, verifyClient, path: opts.path}; + // TODO: precedence, error handling middleware + const run_middleware = (request, response) => { + let cb = () => { + response.end(new Error('No terminal middleware to handle the request.')); + }; + + for (let i = this._middlewares.length - 1; i >= 0; --i) { + const mw = this._middlewares[i]; + const old_cb = cb; + cb = (maybeErr) => { + if (maybeErr instanceof Error) { + response.end(maybeErr); + } else { + try { + mw(request, response, old_cb); + } catch (err) { + response.end(err); + } + } + }; + } + cb(); + }; + // RSI: only become ready when this and metadata are both ready. const add_websocket = (server) => { const ws_server = new websocket.Server(Object.assign({server}, ws_options)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => { try { - const client = new Client(socket, this, () => ...); + const client = new Client(socket, + this._auth, + this._reliable_metadata, + run_middleware); this._clients.add(client); socket.on('close', () => this._clients.delete(client)); - } catch (e) { - // RSI: do something. + } catch (err) { + logger.error(`Failed to construct client: ${err}`); + if (socket.readyState === websocket.OPEN) { + socket.close(1002, err.message.substr(0, 64)); + } } }); From 2651ec172850ebb31df5f904d4ea76a0bf021866 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Wed, 17 Aug 2016 00:04:35 +0000 Subject: [PATCH 14/86] it build! --- server/package.json | 1 + server/src/client.js | 16 +- server/src/metadata/reliable_metadata.js | 7 +- server/src/reliable.js | 6 +- server/src/server.js | 58 +++---- server/test/http_tests.js | 12 +- server/test/permissions.js | 184 +++++++++++------------ server/test/prereq_tests.js | 4 +- server/test/protocol_tests.js | 10 +- server/test/query_tests.js | 68 ++++----- server/test/schema.js | 48 +++--- server/test/subscribe_tests.js | 2 +- server/test/utils.js | 24 +-- server/test/write_tests.js | 174 ++++++++++----------- test/serve.js | 4 +- 15 files changed, 314 insertions(+), 304 deletions(-) diff --git a/server/package.json b/server/package.json index 3fd5f2e92..4eb25ea57 100644 --- a/server/package.json +++ b/server/package.json @@ -41,6 +41,7 @@ "mocha": "^2.3.3", "nodemon": "^1.8.1", "babel-cli": "^6.10.1", + "source-map-support": "^0.4.0", "babel-preset-es2015": "^6.9.0" } } diff --git a/server/src/client.js b/server/src/client.js index 15597635d..b3d527cd0 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -54,9 +54,11 @@ class Client { cb(); } catch (err) { logger.debug(`Unhandled error in request: ${err.stack}`); - this.close({request_id: null, - error: `Unhandled error: ${err}`, - error_code: 0}); + this.close({ + request_id: null, + error: `Unhandled error: ${err}`, + error_code: 0, + }); } } @@ -65,9 +67,11 @@ class Client { try { request = JSON.parse(data); } catch (err) { - return this.close({request_id: null, - error: `Invalid JSON: ${err}`, - error_code: 0}); + return this.close({ + request_id: null, + error: `Invalid JSON: ${err}`, + error_code: 0, + }); } try { diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index ba7364de5..b67ff800b 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -1,7 +1,6 @@ 'use strict'; const error = require('../error'); -const version_field = require('../endpoints/writes').version_field; const {Reliable, ReliableChangefeed, ReliableUnion} = require('../reliable'); const logger = require('../logger'); const Collection = require('./collection').Collection; @@ -9,6 +8,7 @@ const utils = require('../utils'); const r = require('rethinkdb'); +const version_field = '$hz_v$'; const metadata_version = [ 2, 0, 0 ]; const create_collection = (db, name, conn) => @@ -145,7 +145,7 @@ class ReliableInit extends Reliable { }).catch((err) => { if (!(err instanceof StaleAttemptError)) { logger.debug(`Metadata initialization failed: ${err}`); - setTimeout(1000, () => { this.do_init(conn, attempt); }); + setTimeout(() => { this.do_init(conn, attempt); }, 1000); } }); } @@ -170,7 +170,8 @@ class ReliableMetadata extends Reliable { this._auto_create_index = auto_create_index; this._collections = new Map(); - this._reliable_init = new ReliableInit(reliable_conn); + this._reliable_init = new ReliableInit( + this._db, reliable_conn, auto_create_collection); this._collection_changefeed = new ReliableChangefeed( r.db(this._db) diff --git a/server/src/reliable.js b/server/src/reliable.js index 91989a4a0..22c56ae62 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -98,7 +98,7 @@ class ReliableConn extends Reliable { logger.error(`Error in ${JSON.stringify(this)}: ${e.stack}`); } if (!this.closed) { - setTimeout(1000, () => this.connect()); + setTimeout(() => this.connect(), 1000); } }); } @@ -112,7 +112,7 @@ class ReliableConn extends Reliable { } } -class ReliableCfeed extends Reliable { +class ReliableChangefeed extends Reliable { constructor(reql, reliableConn, cbs) { super(cbs); this.reql = reql; @@ -197,6 +197,6 @@ class ReliableUnion extends Reliable { module.exports = { Reliable, ReliableConn, - ReliableCfeed, + ReliableChangefeed, ReliableUnion, }; diff --git a/server/src/server.js b/server/src/server.js index f582a9b5e..f1e1d364d 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -32,6 +32,10 @@ class Server extends EventEmitter { this._ws_servers = [ ]; this._close_promise = null; this._middlewares = [ ]; + this._default_run_middleware = (req, res, next) => { + next(new Error('No terminal middleware to handle the request.')); + }; + this._run_middleware = this._default_run_middleware; this._reliable_conn = new ReliableConn({ host: opts.rbd_host, @@ -74,40 +78,18 @@ class Server extends EventEmitter { const ws_options = {handleProtocols, verifyClient, path: opts.path}; - // TODO: precedence, error handling middleware - const run_middleware = (request, response) => { - let cb = () => { - response.end(new Error('No terminal middleware to handle the request.')); - }; - - for (let i = this._middlewares.length - 1; i >= 0; --i) { - const mw = this._middlewares[i]; - const old_cb = cb; - cb = (maybeErr) => { - if (maybeErr instanceof Error) { - response.end(maybeErr); - } else { - try { - mw(request, response, old_cb); - } catch (err) { - response.end(err); - } - } - }; - } - cb(); - }; - // RSI: only become ready when this and metadata are both ready. const add_websocket = (server) => { const ws_server = new websocket.Server(Object.assign({server}, ws_options)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => { try { - const client = new Client(socket, - this._auth, - this._reliable_metadata, - run_middleware); + const client = new Client( + socket, + this._auth, + this._reliable_metadata, + (...rest) => this._run_middleware.apply(this, rest) + ); this._clients.add(client); socket.on('close', () => this._clients.delete(client)); } catch (err) { @@ -138,6 +120,26 @@ class Server extends EventEmitter { add_middleware(mw) { this._middlewares.push(mw); + this._run_middleware = this._default_run_middleware; + for (let i = this._middlewares.length - 1; i >= 0; --i) { + const mw = this._middlewares[i]; + const old_cb = this._run_middleware; + this._run_middleware = (req, res, next) => { + try { + mw(req, + res, + (maybeErr) => { + if (maybeErr instanceof Error) { + next(maybeErr); + } else { + old_cb(req, res, next); + } + }); + } catch (e) { + next(e); + } + }; + } } // TODO: We close clients in `onUnready` above, but don't wait for diff --git a/server/test/http_tests.js b/server/test/http_tests.js index c119d59e4..dd17b8b0a 100644 --- a/server/test/http_tests.js +++ b/server/test/http_tests.js @@ -46,12 +46,12 @@ const all_tests = () => { if (transport === 'http') { http_server = new http.createServer(four_o_four); } else { - http_server = new https.createServer({ key: fs.readFileSync(key_file), - cert: fs.readFileSync(cert_file) }, + http_server = new https.createServer({key: fs.readFileSync(key_file), + cert: fs.readFileSync(cert_file)}, four_o_four); } - horizon(http_server, { auth: { token_secret: 'hunter2' } }); + horizon(http_server, {auth: {token_secret: 'hunter2'}}); http_server.listen(0, done); }); @@ -61,10 +61,10 @@ const all_tests = () => { }); it('localhost/horizon/horizon.js', (done) => { - require(transport).get({ host: 'localhost', + require(transport).get({host: 'localhost', port: http_server.address().port, path: '/horizon/horizon.js', - rejectUnauthorized: false }, (res) => { + rejectUnauthorized: false}, (res) => { const client_js = path.resolve(__dirname, '../node_modules/@horizon/client/dist/horizon.js'); const code = fs.readFileSync(client_js); let buffer = ''; @@ -79,4 +79,4 @@ const all_tests = () => { const suite = (collection) => describe('Webserver', () => all_tests(collection)); -module.exports = { suite }; +module.exports = {suite}; diff --git a/server/test/permissions.js b/server/test/permissions.js index 9c3e06eb8..8d9b9876c 100644 --- a/server/test/permissions.js +++ b/server/test/permissions.js @@ -6,24 +6,24 @@ const assert = require('assert'); const make_request = (type, collection, options) => { if (collection !== null) { - return { request_id: 5, type, options: Object.assign({ collection }, options) }; + return {request_id: 5, type, options: Object.assign({collection}, options)}; } else { - return { request_id: 5, type, options }; + return {request_id: 5, type, options}; } }; -const context = { id: 123, groups: [ 'admin', 'default', 'authenticated' ] }; +const context = {id: 123, groups: [ 'admin', 'default', 'authenticated' ]}; describe('Permissions', () => { describe('Template', () => { it('any', () => { - const rule = new Rule('foo', { template: 'any()' }); + const rule = new Rule('foo', {template: 'any()'}); const tests = [ { }, - { type: 'query', options: { collection: 'test' } }, - { fake: 'bar' }, - { options: { } }, - { type: 'query', options: { fake: 'baz' } } ]; + {type: 'query', options: {collection: 'test'}}, + {fake: 'bar'}, + {options: { }}, + {type: 'query', options: {fake: 'baz'}} ]; for (const t of tests) { assert(rule.is_match(t, context)); @@ -32,24 +32,24 @@ describe('Permissions', () => { }); it('any read', () => { - const rule = new Rule('foo', { template: 'collection(any()).anyRead()' }); + const rule = new Rule('foo', {template: 'collection(any()).anyRead()'}); assert(rule.is_valid()); assert(!rule.is_match(make_request('fake', 'test', { }), context)); assert(!rule.is_match(make_request('store', 'test', { }), context)); assert(!rule.is_match(make_request('query', null, { }), context)); assert(rule.is_match(make_request('query', 'fake', { }), context)); - assert(rule.is_match(make_request('query', 'fake', { find: { } }), context)); - assert(rule.is_match(make_request('query', 'test', { bar: 'baz' }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { }, { } ] }), context)); + assert(rule.is_match(make_request('query', 'fake', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [ { }, { } ]}), context)); assert(!rule.is_match(make_request('subscribe', null, { }), context)); assert(rule.is_match(make_request('subscribe', 'fake', { }), context)); - assert(rule.is_match(make_request('subscribe', 'fake', { find: { } }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { bar: 'baz' }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { find_all: [ { }, { } ] }), context)); + assert(rule.is_match(make_request('subscribe', 'fake', {find: { }}), context)); + assert(rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); + assert(rule.is_match(make_request('subscribe', 'test', {find_all: [ { }, { } ]}), context)); }); it('any read with collection', () => { - const rule = new Rule('foo', { template: 'collection("test").anyRead()' }); + const rule = new Rule('foo', {template: 'collection("test").anyRead()'}); assert(rule.is_valid()); assert(!rule.is_match(make_request('query', 'fake', { }), context)); assert(rule.is_match(make_request('query', 'test', { }), context)); @@ -63,108 +63,108 @@ describe('Permissions', () => { it('any read with order', () => { // TODO: allow for any number of fields in order - const rule = new Rule('foo', { template: 'collection("test").order(any(), any()).anyRead()' }); + const rule = new Rule('foo', {template: 'collection("test").order(any(), any()).anyRead()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { order: [ 'foo', 'ascending' ] }), context)); + assert(!rule.is_match(make_request('query', 'fake', {order: [ 'foo', 'ascending' ]}), context)); assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(!rule.is_match(make_request('query', 'test', { order: [ 'baz' ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { order: [ 'baz', 'fake' ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { order: [ [ 'fake' ] ] }), context)); - assert(rule.is_match(make_request('query', 'test', { order: [ [ 'foo' ], 'ascending' ] }), context)); - assert(rule.is_match(make_request('query', 'test', { order: [ [ 'bar' ], 'descending' ] }), context)); - assert(rule.is_match(make_request('query', 'test', { order: [ [ 'baz' ], 'fake' ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find: { }, order: [ [ 'baz' ], 'fake' ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { } ], order: [ [ 'baz' ], 'fake' ] }), context)); - assert(rule.is_match(make_request('query', 'test', { fake: 'baz', order: [ [ 'baz' ], 'fake' ] }), context)); + assert(!rule.is_match(make_request('query', 'test', {order: [ 'baz' ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: [ 'baz', 'fake' ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: [ [ 'fake' ] ]}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [ [ 'foo' ], 'ascending' ]}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [ [ 'bar' ], 'descending' ]}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [ [ 'baz' ], 'fake' ]}), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }, order: [ [ 'baz' ], 'fake' ]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [ { } ], order: [ [ 'baz' ], 'fake' ]}), context)); + assert(rule.is_match(make_request('query', 'test', {fake: 'baz', order: [ [ 'baz' ], 'fake' ]}), context)); }); it('any read with find', () => { - const rule = new Rule('foo', { template: 'collection("test").find(any()).anyRead()' }); + const rule = new Rule('foo', {template: 'collection("test").find(any()).anyRead()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { find: { } }), context)); + assert(!rule.is_match(make_request('query', 'fake', {find: { }}), context)); assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', { find: { } }), context)); - assert(rule.is_match(make_request('query', 'test', { find: { }, fake: 'baz' }), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }, fake: 'baz'}), context)); }); it('any read with findAll', () => { // TODO: allow for any number of arguments in findAll - const rule = new Rule('foo', { template: 'collection("test").findAll(any()).anyRead()' }); + const rule = new Rule('foo', {template: 'collection("test").findAll(any()).anyRead()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { find_all: { } }), context)); + assert(!rule.is_match(make_request('query', 'fake', {find_all: { }}), context)); assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { } ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { } ], fake: 'baz' }), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [ { } ]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [ { } ], fake: 'baz'}), context)); }); it('single key in findAll', () => { - const rule = new Rule('foo', { template: 'collection("test").findAll({ owner: userId() }).fetch()' }); + const rule = new Rule('foo', {template: 'collection("test").findAll({ owner: userId() }).fetch()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: (context.id + 1) } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, bar: 'baz' } ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id } ] }), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ {bar: 'baz'} ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: (context.id + 1)} ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id, bar: 'baz'} ]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id} ]}), context)); }); it('multiple keys in findAll', () => { - const rule = new Rule('foo', { template: 'collection("test").findAll({ owner: userId(), key: any() }).fetch()' }); + const rule = new Rule('foo', {template: 'collection("test").findAll({ owner: userId(), key: any() }).fetch()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: (context.id + 1) } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, bar: 'baz' } ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, key: 123 } ] }), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ {bar: 'baz'} ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: (context.id + 1)} ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id, bar: 'baz'} ]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id, key: 123} ]}), context)); }); it('collection fetch', () => { - const rule = new Rule('foo', { template: 'collection("test").fetch()' }); + const rule = new Rule('foo', {template: 'collection("test").fetch()'}); assert(rule.is_valid()); assert(!rule.is_match(make_request('query', 'fake', { }), context)); - assert(!rule.is_match(make_request('query', 'test', { bar: 'baz' }), context)); - assert(!rule.is_match(make_request('query', 'test', { find: { id: 5 } }), context)); + assert(!rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {id: 5}}), context)); assert(rule.is_match(make_request('query', 'test', { }), context)); }); it('collection watch', () => { - const rule = new Rule('foo', { template: 'collection("test").watch()' }); + const rule = new Rule('foo', {template: 'collection("test").watch()'}); assert(rule.is_valid()); assert(!rule.is_match(make_request('subscribe', 'fake', { }), context)); - assert(!rule.is_match(make_request('subscribe', 'test', { bar: 'baz' }), context)); - assert(!rule.is_match(make_request('subscribe', 'test', { find: { id: 5 } }), context)); + assert(!rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); + assert(!rule.is_match(make_request('subscribe', 'test', {find: {id: 5}}), context)); assert(rule.is_match(make_request('subscribe', 'test', { }), context)); }); for (const type of [ 'store', 'update', 'insert', 'upsert', 'replace', 'remove' ]) { it(`collection ${type}`, () => { - const rule = new Rule('foo', { template: `collection("test").${type}(any())` }); + const rule = new Rule('foo', {template: `collection("test").${type}(any())`}); assert(rule.is_valid()); assert(!rule.is_match(make_request(type, 'test', { }), context)); - assert(!rule.is_match(make_request(type, 'test', { data: { } }), context)); - assert(!rule.is_match(make_request(type, 'test', { data: [ ] }), context)); - assert(!rule.is_match(make_request(type, 'fake', { data: [ { } ] }), context)); - assert(!rule.is_match(make_request(type, 'test', { data: [ { } ], fake: 6 }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ { } ] }), context)); + assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [ ]}), context)); + assert(!rule.is_match(make_request(type, 'fake', {data: [ { } ]}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [ { } ], fake: 6}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ { } ]}), context)); }); it(`collection ${type} batch`, () => { - const rule = new Rule('foo', { template: `collection("test").${type}(anyArray(any()))` }); + const rule = new Rule('foo', {template: `collection("test").${type}(anyArray(any()))`}); assert(rule.is_valid()); assert(!rule.is_match(make_request(type, 'test', { }), context)); - assert(!rule.is_match(make_request(type, 'test', { data: { } }), context)); - assert(!rule.is_match(make_request(type, 'test', { data: [ { } ], fake: 6 }), context)); - assert(!rule.is_match(make_request(type, 'fake', { data: [ { } ] }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ ] }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ { } ] }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ { }, { bar: 'baz' } ] }), context)); + assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [ { } ], fake: 6}), context)); + assert(!rule.is_match(make_request(type, 'fake', {data: [ { } ]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ ]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ { } ]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ { }, {bar: 'baz'} ]}), context)); }); } it('any write', () => { - const rule = new Rule('foo', { template: 'collection("test").anyWrite()' }); + const rule = new Rule('foo', {template: 'collection("test").anyWrite()'}); assert(rule.is_valid()); assert(!rule.is_match(make_request('fake', 'test', { }), context)); assert(!rule.is_match(make_request('query', 'test', { }), context)); @@ -172,44 +172,44 @@ describe('Permissions', () => { for (const type of [ 'store', 'update', 'insert', 'upsert', 'replace', 'remove' ]) { assert(!rule.is_match(make_request(type, 'fake', { }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ ] }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ { } ] }), context)); - assert(rule.is_match(make_request(type, 'test', { data: [ ], bar: 'baz' }), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ ]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ { } ]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [ ], bar: 'baz'}), context)); } }); it('userId in find', () => { - const rule = new Rule('foo', { template: 'collection("test").find({ owner: userId() }).fetch()' }); + const rule = new Rule('foo', {template: 'collection("test").find({ owner: userId() }).fetch()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find: { } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find: true }), context)); - assert(!rule.is_match(make_request('query', 'test', { find: [ ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find: { bar: 'baz' } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find: { owner: (context.id + 1) } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find: { owner: context.id, bar: 'baz' } }), context)); - assert(rule.is_match(make_request('query', 'test', { find: { owner: context.id } }), context)); + assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: [ ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); + assert(rule.is_match(make_request('query', 'test', {find: {owner: context.id}}), context)); }); it('adds readAny() implicitly', () => { { - const rule = new Rule('foo', { template: 'collection("test")' }); + const rule = new Rule('foo', {template: 'collection("test")'}); assert(rule.is_valid()); - assert(rule.is_match(make_request('query', 'test', { find: { } }), context)); - assert(rule.is_match(make_request('query', 'test', { find: { bar: 'baz' } }), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); } { - const rule = new Rule('foo', { template: 'collection("test").find({bar: any()})' }); + const rule = new Rule('foo', {template: 'collection("test").find({bar: any()})'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find: { } }), context)); - assert(rule.is_match(make_request('query', 'test', { find: { bar: 'baz' } }), context)); + assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); } }); it('error on incomplete template', () => { - assert.throws(() => new Rule('foo', { template: '({ })' }), /Incomplete template/); - assert.throws(() => new Rule('foo', { template: '[ ]' }), /Invalid template/); - assert.throws(() => new Rule('foo', { template: '5' }), /Invalid template/); - assert.throws(() => new Rule('foo', { template: 'null' }), /Invalid template/); + assert.throws(() => new Rule('foo', {template: '({ })'}), /Incomplete template/); + assert.throws(() => new Rule('foo', {template: '[ ]'}), /Invalid template/); + assert.throws(() => new Rule('foo', {template: '5'}), /Invalid template/); + assert.throws(() => new Rule('foo', {template: 'null'}), /Invalid template/); }); }); }); diff --git a/server/test/prereq_tests.js b/server/test/prereq_tests.js index 70549411e..bc8c6b7b3 100644 --- a/server/test/prereq_tests.js +++ b/server/test/prereq_tests.js @@ -20,7 +20,7 @@ const all_tests = (collection) => { let finished = 0; for (let i = 0; i < query_count; ++i) { utils.stream_test( - { request_id: i, type: 'query', options: { collection: rand_collection } }, + {request_id: i, type: 'query', options: {collection: rand_collection}}, (err, res) => { assert.ifError(err); assert.strictEqual(res.length, 0); @@ -100,4 +100,4 @@ const all_tests = (collection) => { const suite = (collection) => describe('Prereqs', () => all_tests(collection)); -module.exports = { suite }; +module.exports = {suite}; diff --git a/server/test/protocol_tests.js b/server/test/protocol_tests.js index 5a4521036..4f6fa71f3 100644 --- a/server/test/protocol_tests.js +++ b/server/test/protocol_tests.js @@ -30,7 +30,7 @@ const all_tests = (collection) => { }); it('no type', (done) => { - utils.stream_test({ request_id: 0 }, (err, res) => { + utils.stream_test({request_id: 0}, (err, res) => { assert.deepStrictEqual(res, [ ]); utils.check_error(err, '"type" is required'); done(); @@ -38,7 +38,7 @@ const all_tests = (collection) => { }); it('no options', (done) => { - utils.stream_test({ request_id: 1, type: 'fake' }, (err, res) => { + utils.stream_test({request_id: 1, type: 'fake'}, (err, res) => { assert.deepStrictEqual(res, [ ]); utils.check_error(err, '"options" is required'); done(); @@ -46,7 +46,7 @@ const all_tests = (collection) => { }); it('invalid endpoint', (done) => { - utils.stream_test({ request_id: 2, type: 'fake', options: { } }, (err, res) => { + utils.stream_test({request_id: 2, type: 'fake', options: { }}, (err, res) => { assert.deepStrictEqual(res, [ ]); assert.strictEqual(err.message, '"fake" is not a registered request type.'); done(); @@ -62,7 +62,7 @@ const all_tests = (collection) => { { request_id: 3, type: 'subscribe', - options: { collection }, + options: {collection}, })); utils.add_horizon_listener(3, (msg) => { if (msg.error !== undefined) { @@ -93,4 +93,4 @@ const all_tests = (collection) => { const suite = (collection) => describe('Protocol', () => all_tests(collection)); -module.exports = { suite }; +module.exports = {suite}; diff --git a/server/test/query_tests.js b/server/test/query_tests.js index 98e2c17cd..63e1a5f7f 100644 --- a/server/test/query_tests.js +++ b/server/test/query_tests.js @@ -17,7 +17,7 @@ const all_tests = (collection) => { { request_id: 0, type: 'query', - options: { collection }, + options: {collection}, }, (err, res) => { assert.ifError(err); @@ -85,7 +85,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - above: [ { id: 5 }, 'closed' ], + above: [ {id: 5}, 'closed' ], }, }, (err, res) => { @@ -102,7 +102,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - below: [ { id: 5 }, 'closed' ], + below: [ {id: 5}, 'closed' ], }, }, (err, res) => { @@ -119,8 +119,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - above: [ { id: 5 }, 'open' ], - below: [ { id: 7 }, 'open' ], + above: [ {id: 5}, 'open' ], + below: [ {id: 7}, 'open' ], }, }, (err, res) => { @@ -137,7 +137,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find: { id: 4 }, + find: {id: 4}, }, }, (err, res) => { @@ -154,7 +154,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find: { id: 14 }, + find: {id: 14}, }, }, (err, res) => { @@ -171,7 +171,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { id: 4 }, { id: 6 }, { id: 9 } ], + find_all: [ {id: 4}, {id: 6}, {id: 9} ], }, }, (err, res) => { @@ -188,7 +188,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { id: 1 } ], + find_all: [ {id: 1} ], order: [ [ 'value' ], 'descending' ], }, }, @@ -206,7 +206,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { id: 4 }, { id: 8 }, { id: 2 }, { id: 1 } ], + find_all: [ {id: 4}, {id: 8}, {id: 2}, {id: 1} ], limit: 3, }, }, @@ -224,7 +224,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { id: 4 } ], + find_all: [ {id: 4} ], order: [ [ 'value' ], 'descending' ], limit: 3, }, @@ -243,8 +243,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 1 } ], - above: [ { id: 3 }, 'open' ], + find_all: [ {value: 1} ], + above: [ {id: 3}, 'open' ], }, }, (err, res) => { @@ -261,8 +261,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 1 } ], - below: [ { id: 5 }, 'open' ], + find_all: [ {value: 1} ], + below: [ {id: 5}, 'open' ], }, }, (err, res) => { @@ -279,9 +279,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 1 } ], - above: [ { id: 1 }, 'closed' ], - below: [ { id: 9 }, 'open' ], + find_all: [ {value: 1} ], + above: [ {id: 1}, 'closed' ], + below: [ {id: 9}, 'open' ], }, }, (err, res) => { @@ -298,9 +298,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 1 } ], + find_all: [ {value: 1} ], order: [ [ 'id' ], 'ascending' ], - above: [ { id: 7 }, 'open' ], + above: [ {id: 7}, 'open' ], }, }, (err, res) => { @@ -317,9 +317,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 0 } ], + find_all: [ {value: 0} ], order: [ [ 'id' ], 'descending' ], - below: [ { id: 8 }, 'open' ], + below: [ {id: 8}, 'open' ], }, }, (err, res) => { @@ -336,10 +336,10 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 0 } ], + find_all: [ {value: 0} ], order: [ [ 'id' ], 'descending' ], - above: [ { id: 3 }, 'closed' ], - below: [ { id: 9 }, 'closed' ], + above: [ {id: 3}, 'closed' ], + below: [ {id: 9}, 'closed' ], }, }, (err, res) => { @@ -358,9 +358,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 0 } ], + find_all: [ {value: 0} ], order: [ [ 'value', 'a' ], 'descending' ], - above: [ { b: 4 }, 'closed' ], + above: [ {b: 4}, 'closed' ], }, }, (err) => { @@ -376,9 +376,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 0 } ], + find_all: [ {value: 0} ], order: [ [ 'value', 'a' ], 'descending' ], - above: [ { a: 4 }, 'closed' ], + above: [ {a: 4}, 'closed' ], }, }, (err) => { @@ -394,9 +394,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 0 } ], + find_all: [ {value: 0} ], order: [ [ 'value', 'a' ], 'descending' ], - below: [ { b: 4 }, 'closed' ], + below: [ {b: 4}, 'closed' ], }, }, (err) => { @@ -412,9 +412,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ { value: 0 } ], + find_all: [ {value: 0} ], order: [ [ 'value', 'a' ], 'descending' ], - below: [ { a: 4 }, 'closed' ], + below: [ {a: 4}, 'closed' ], }, }, (err) => { @@ -426,4 +426,4 @@ const all_tests = (collection) => { const suite = (collection) => describe('Query', () => all_tests(collection)); -module.exports = { suite }; +module.exports = {suite}; diff --git a/server/test/schema.js b/server/test/schema.js index 4c9bdd8df..d8b908cc5 100644 --- a/server/test/schema.js +++ b/server/test/schema.js @@ -70,12 +70,12 @@ describe('Schema', () => { describe('Write', () => { const write_without_id = { collection: 'horizon', - data: [ { field: 4 } ], + data: [ {field: 4} ], }; const write_with_id = { collection: 'horizon', - data: [ { id: 5, field: 4 } ], + data: [ {id: 5, field: 4} ], }; // In order to reduce the number of tests, these were written assuming @@ -242,7 +242,7 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); request.order = [ [ 'id' ], 'ascending' ]; - request.above = [ { id: 10 }, 'open' ]; + request.above = [ {id: 10}, 'open' ]; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -251,7 +251,7 @@ describe('Schema', () => { it('below', () => { const request = Object.assign({}, valid); request.order = [ [ 'id' ], 'ascending' ]; - request.below = [ { id: 5 }, 'open' ]; + request.below = [ {id: 5}, 'open' ]; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -268,8 +268,8 @@ describe('Schema', () => { it('above and below and limit', () => { const request = Object.assign({}, valid); request.order = [ [ 'id' ], 'ascending' ]; - request.below = [ { id: 0 }, 'closed' ]; - request.below = [ { id: 5 }, 'closed' ]; + request.below = [ {id: 0}, 'closed' ]; + request.below = [ {id: 5}, 'closed' ]; request.limit = 4; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); @@ -324,7 +324,7 @@ describe('Schema', () => { it('"above" without "order"', () => { const request = Object.assign({}, valid); - request.above = [ { id: 5 }, 'open' ]; + request.above = [ {id: 5}, 'open' ]; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -352,11 +352,11 @@ describe('Schema', () => { const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"value" must have 1 child'); } { - request.above = [ { id: 4 }, 5 ]; + request.above = [ {id: 4}, 5 ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be a string'); } { - request.above = [ { id: 3 }, 'ajar' ]; + request.above = [ {id: 3}, 'ajar' ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be one of [open, closed]'); } @@ -364,7 +364,7 @@ describe('Schema', () => { it('"below" without "order"', () => { const request = Object.assign({}, valid); - request.below = [ { id: 1 }, 'open' ]; + request.below = [ {id: 1}, 'open' ]; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -394,11 +394,11 @@ describe('Schema', () => { const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"value" must have 1 child'); } { - request.below = [ { id: 4 }, 5 ]; + request.below = [ {id: 4}, 5 ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be a string'); } { - request.below = [ { id: 3 }, 'ajar' ]; + request.below = [ {id: 3}, 'ajar' ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be one of [open, closed]'); } @@ -428,7 +428,7 @@ describe('Schema', () => { describe('find', () => { const valid = { collection: 'horizon', - find: { score: 4 }, + find: {score: 4}, }; it('valid', () => { @@ -446,14 +446,14 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); - request.above = [ { id: 3 }, 'open' ]; + request.above = [ {id: 3}, 'open' ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"above" is not allowed'); }); it('below', () => { const request = Object.assign({}, valid); - request.below = [ { id: 4 }, 'closed' ]; + request.below = [ {id: 4}, 'closed' ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"below" is not allowed'); }); @@ -476,7 +476,7 @@ describe('Schema', () => { describe('find_all multiple', () => { const valid = { collection: 'horizon', - find_all: [ { score: 2 }, { score: 5, id: 0 } ], + find_all: [ {score: 2}, {score: 5, id: 0} ], }; it('valid', () => { @@ -503,7 +503,7 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); { - request.above = [ { id: 3 }, 'closed' ]; + request.above = [ {id: 3}, 'closed' ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"above" is not allowed'); } { @@ -516,7 +516,7 @@ describe('Schema', () => { it('below', () => { const request = Object.assign({}, valid); { - request.below = [ { id: 9 }, 'closed' ]; + request.below = [ {id: 9}, 'closed' ]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"below" is not allowed'); } { @@ -530,7 +530,7 @@ describe('Schema', () => { describe('find_all one', () => { const valid = { collection: 'horizon', - find_all: [ { score: 8, id: 5 } ], + find_all: [ {score: 8, id: 5} ], }; it('valid', () => { @@ -558,7 +558,7 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); request.order = [ [ 'id' ], 'ascending' ]; - request.above = [ { id: 3 }, 'closed' ]; + request.above = [ {id: 3}, 'closed' ]; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -567,7 +567,7 @@ describe('Schema', () => { it('below', () => { const request = Object.assign({}, valid); request.order = [ [ 'id' ], 'descending' ]; - request.below = [ { id: 9 }, 'closed' ]; + request.below = [ {id: 9}, 'closed' ]; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -576,8 +576,8 @@ describe('Schema', () => { it('above and below and limit', () => { const request = Object.assign({}, valid); request.order = [ [ 'id' ], 'descending' ]; - request.above = [ { id: 'foo' }, 'open' ]; - request.below = [ { id: 'bar' }, 'closed' ]; + request.above = [ {id: 'foo'}, 'open' ]; + request.below = [ {id: 'bar'}, 'closed' ]; request.limit = 59; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); @@ -606,7 +606,7 @@ describe('Schema', () => { it('with "find"', () => { const request = Object.assign({}, valid); - request.find = { id: 7 }; + request.find = {id: 7}; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"find" is not allowed'); // TODO: better message? }); diff --git a/server/test/subscribe_tests.js b/server/test/subscribe_tests.js index f5299e591..e05427758 100644 --- a/server/test/subscribe_tests.js +++ b/server/test/subscribe_tests.js @@ -12,4 +12,4 @@ const all_tests = (collection) => { const suite = (collection) => describe('Subscribe', () => all_tests(collection)); -module.exports = { suite }; +module.exports = {suite}; diff --git a/server/test/utils.js b/server/test/utils.js index 41121c2b2..df436b8b0 100644 --- a/server/test/utils.js +++ b/server/test/utils.js @@ -17,7 +17,7 @@ const data_dir = './rethinkdb_data_test'; const log_file = `./horizon_test_${process.pid}.log`; logger.level = 'debug'; -logger.add(logger.transports.File, { filename: log_file }); +logger.add(logger.transports.File, {filename: log_file}); logger.remove(logger.transports.Console); // Variables used by most tests @@ -29,13 +29,13 @@ const start_rethinkdb = () => { rm_sync_recursive(data_dir); logger.info('creating server'); - return start_rdb_server({ dataDir: data_dir }).then((server) => { + return start_rdb_server({dataDir: data_dir}).then((server) => { rdb_server = server; rdb_port = server.driver_port; rdb_http_port = server.http_port; logger.info('server created, connecting'); - return r.connect({ db: project_name, port: rdb_port }); + return r.connect({db: project_name, port: rdb_port}); }).then((conn) => { logger.info('connected'); rdb_conn = conn; @@ -61,7 +61,7 @@ const table = (collection) => const make_admin_token = () => { const jwt = horizon_server && horizon_server._auth && horizon_server._auth._jwt; assert(jwt); - return jwt.sign({ id: 'admin', provider: null }).token; + return jwt.sign({id: 'admin', provider: null}).token; }; // Creates a collection, no-op if it already exists, uses horizon server prereqs @@ -70,10 +70,10 @@ const create_collection = (collection, done) => { assert.notStrictEqual(horizon_port, undefined); const conn = new websocket(`ws://localhost:${horizon_port}/horizon`, horizon.protocol, - { rejectUnauthorized: false }) + {rejectUnauthorized: false}) .once('error', (err) => assert.ifError(err)) .on('open', () => { - conn.send(JSON.stringify({ request_id: 123, method: 'token', token: make_admin_token() })); + conn.send(JSON.stringify({request_id: 123, method: 'token', token: make_admin_token()})); conn.once('message', (data) => { const res = JSON.parse(data); assert.strictEqual(res.request_id, 123); @@ -85,7 +85,7 @@ const create_collection = (collection, done) => { conn.send(JSON.stringify({ request_id: 0, type: 'query', - options: { collection, limit: 0 }, + options: {collection, limit: 0}, })); conn.once('message', () => { @@ -110,7 +110,7 @@ const populate_collection = (collection, rows, done) => { if (rows.constructor.name !== 'Array') { table(collection).insert( r.range(rows).map( - (i) => ({ id: i, value: i.mod(4) }) + (i) => ({id: i, value: i.mod(4)}) )).run(rdb_conn).then(() => done()); } else { table(collection).insert(rows).run(rdb_conn).then(() => done()); @@ -126,7 +126,7 @@ const start_horizon_server = (done) => { logger.info('creating horizon server'); horizon_port = http_server.address().port; horizon_server = new horizon.Server(http_server, - { project_name, + {project_name, rdb_port, auto_create_collection: true, auto_create_index: true, @@ -183,7 +183,7 @@ const open_horizon_conn = (done) => { horizon_conn = new websocket(`ws://localhost:${horizon_port}/horizon`, horizon.protocol, - { rejectUnauthorized: false }) + {rejectUnauthorized: false}) .once('error', (err) => assert.ifError(err)) .on('open', () => done()); }; @@ -209,7 +209,7 @@ const horizon_auth = (req, cb) => { // Create a token for the admin user and use that to authenticate const horizon_admin_auth = (done) => { - horizon_auth({ request_id: -1, method: 'token', token: make_admin_token() }, (res) => { + horizon_auth({request_id: -1, method: 'token', token: make_admin_token()}, (res) => { assert.strictEqual(res.request_id, -1); assert.strictEqual(typeof res.token, 'string'); assert.strictEqual(res.id, 'admin'); @@ -219,7 +219,7 @@ const horizon_admin_auth = (done) => { }; const horizon_default_auth = (done) => { - horizon_auth({ request_id: -1, method: 'unauthenticated' }, (res) => { + horizon_auth({request_id: -1, method: 'unauthenticated'}, (res) => { assert.strictEqual(res.request_id, -1); assert.strictEqual(typeof res.token, 'string'); assert.strictEqual(res.id, null); diff --git a/server/test/write_tests.js b/server/test/write_tests.js index 1792e05ea..ae2d4b040 100644 --- a/server/test/write_tests.js +++ b/server/test/write_tests.js @@ -11,10 +11,10 @@ const invalidated_msg = horizon_writes.invalidated_msg; // Before each test, ids [0, 4) will be present in the collection const original_data = [ - { id: 0, old_field: [ ], [hz_v]: 0 }, - { id: 1, old_field: [ ], [hz_v]: 0 }, - { id: 2, old_field: [ ], [hz_v]: 0 }, - { id: 3, old_field: [ ], [hz_v]: 0 }, + {id: 0, old_field: [ ], [hz_v]: 0}, + {id: 1, old_field: [ ], [hz_v]: 0}, + {id: 2, old_field: [ ], [hz_v]: 0}, + {id: 3, old_field: [ ], [hz_v]: 0}, ]; const new_id = [ 4 ]; @@ -40,20 +40,20 @@ const check_collection_data = (actual, expected) => { // TODO: verify through reql that rows have been inserted/removed const all_tests = (collection) => { - const new_row_from_id = (id) => ({ id, new_field: 'a' }); + const new_row_from_id = (id) => ({id, new_field: 'a'}); const merged_row_from_id = (id) => { if (id >= 4) { return new_row_from_id(id); } - return { id, new_field: 'a', old_field: [ ] }; + return {id, new_field: 'a', old_field: [ ]}; }; const make_request = (type, data, options) => ({ request_id: crypto.randomBytes(4).readUInt32BE(), type, - options: Object.assign({}, options || {}, { collection, data }), + options: Object.assign({}, options || {}, {collection, data}), }); const check_collection = (expected, done) => { - utils.table(collection).orderBy({ index: 'id' }).coerceTo('array') + utils.table(collection).orderBy({index: 'id'}).coerceTo('array') .run(utils.rdb_conn()).then((res) => { check_collection_data(res, expected); done(); @@ -92,7 +92,7 @@ const all_tests = (collection) => { describe('Store', () => { const test_case = (ids, done) => { utils.stream_test(request_from_ids('store', ids), (err, res) => { - const expected = ids.map((id) => ({ id })); + const expected = ids.map((id) => ({id})); assert.ifError(err); compare_write_response(res, expected); const new_data = ids.map(new_row_from_id); @@ -110,7 +110,7 @@ const all_tests = (collection) => { const test_case = (ids, done) => { utils.stream_test(request_from_ids('replace', ids), (err, res) => { const expected = ids.map((id) => - (id < original_data.length ? { id } : { error: 'The document was missing.' }) + (id < original_data.length ? {id} : {error: 'The document was missing.'}) ); assert.ifError(err); compare_write_response(res, expected); @@ -128,7 +128,7 @@ const all_tests = (collection) => { describe('Upsert', () => { const test_case = (ids, done) => { utils.stream_test(request_from_ids('upsert', ids), (err, res) => { - const expected = ids.map((id) => ({ id })); + const expected = ids.map((id) => ({id})); assert.ifError(err); compare_write_response(res, expected); const new_data = ids.map(merged_row_from_id); @@ -146,7 +146,7 @@ const all_tests = (collection) => { const test_case = (ids, done) => { utils.stream_test(request_from_ids('update', ids), (err, res) => { const expected = ids.map((id) => - (id < original_data.length ? { id } : { error: 'The document was missing.' }) + (id < original_data.length ? {id} : {error: 'The document was missing.'}) ); assert.ifError(err); compare_write_response(res, expected); @@ -169,7 +169,7 @@ const all_tests = (collection) => { const test_case = (ids, done) => { utils.stream_test(request_from_ids('insert', ids), (err, res) => { const expected = ids.map((id) => - (id >= original_data.length ? { id } : { error: 'The document already exists.' }) + (id >= original_data.length ? {id} : {error: 'The document already exists.'}) ); assert.ifError(err); @@ -194,7 +194,7 @@ const all_tests = (collection) => { const test_case = (ids, done) => { utils.stream_test(request_from_ids('remove', ids), (err, res) => { - const expected = ids.map((id) => ({ id })); + const expected = ids.map((id) => ({id})); assert.ifError(err); compare_write_response(res, expected); const deleted_data = ids.map(new_row_from_id); @@ -212,25 +212,25 @@ const all_tests = (collection) => { describe('Versioned', () => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); - const test_data = [ { id: 'versioned', [hz_v]: 11, foo: 'bar' } ]; + const test_data = [ {id: 'versioned', [hz_v]: 11, foo: 'bar'} ]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); describe('Store', () => { const request = (row) => make_request('store', [ row ]); it('correct version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versioned', [hz_v]: 12 } ]; + const expected = [ {id: 'versioned', [hz_v]: 12} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versioned', [hz_v]: 12, value: 1 } ], done); + check_collection([ {id: 'versioned', [hz_v]: 12, value: 1} ], done); }); }); it('incorrect version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -241,18 +241,18 @@ const all_tests = (collection) => { const request = (row) => make_request('replace', [ row ]); it('correct version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versioned', [hz_v]: 12 } ]; + const expected = [ {id: 'versioned', [hz_v]: 12} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versioned', [hz_v]: 12, value: 1 } ], done); + check_collection([ {id: 'versioned', [hz_v]: 12, value: 1} ], done); }); }); it('incorrect version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -263,18 +263,18 @@ const all_tests = (collection) => { const request = (row) => make_request('upsert', [ row ]); it('correct version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versioned', [hz_v]: 12 } ]; + const expected = [ {id: 'versioned', [hz_v]: 12} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar' } ], done); + check_collection([ {id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar'} ], done); }); }); it('incorrect version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -285,18 +285,18 @@ const all_tests = (collection) => { const request = (row) => make_request('update', [ row ]); it('correct version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versioned', [hz_v]: 12 } ]; + const expected = [ {id: 'versioned', [hz_v]: 12} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar' } ], done); + check_collection([ {id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar'} ], done); }); }); it('incorrect version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -307,18 +307,18 @@ const all_tests = (collection) => { const request = (row) => make_request('remove', [ row ]); it('correct version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 1, [hz_v]: 11 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versioned', [hz_v]: 11 } ]; + const expected = [ {id: 'versioned', [hz_v]: 11} ]; assert.deepStrictEqual(res, expected); check_collection([ ], done); }); }); it('incorrect version', (done) => { - utils.stream_test(request({ id: 'versioned', value: 2, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -329,25 +329,25 @@ const all_tests = (collection) => { describe('Versionless', () => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); - const test_data = [ { id: 'versionless', foo: 'bar' } ]; + const test_data = [ {id: 'versionless', foo: 'bar'} ]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); describe('Store', () => { const request = (row) => make_request('store', [ row ]); it('unspecified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versionless', [hz_v]: 0 } ]; + const expected = [ {id: 'versionless', [hz_v]: 0} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versionless', [hz_v]: 0, value: 3 } ], done); + check_collection([ {id: 'versionless', [hz_v]: 0, value: 3} ], done); }); }); it('specified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -358,18 +358,18 @@ const all_tests = (collection) => { const request = (row) => make_request('replace', [ row ]); it('unspecified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versionless', [hz_v]: 0 } ]; + const expected = [ {id: 'versionless', [hz_v]: 0} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versionless', [hz_v]: 0, value: 3 } ], done); + check_collection([ {id: 'versionless', [hz_v]: 0, value: 3} ], done); }); }); it('specified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -380,18 +380,18 @@ const all_tests = (collection) => { const request = (row) => make_request('upsert', [ row ]); it('unspecified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versionless', [hz_v]: 0 } ]; + const expected = [ {id: 'versionless', [hz_v]: 0} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar' } ], done); + check_collection([ {id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar'} ], done); }); }); it('specified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -402,18 +402,18 @@ const all_tests = (collection) => { const request = (row) => make_request('update', [ row ]); it('unspecified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versionless', [hz_v]: 0 } ]; + const expected = [ {id: 'versionless', [hz_v]: 0} ]; assert.deepStrictEqual(res, expected); - check_collection([ { id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar' } ], done); + check_collection([ {id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar'} ], done); }); }); it('specified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -424,18 +424,18 @@ const all_tests = (collection) => { const request = (row) => make_request('remove', [ row ]); it('unspecified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 3 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ { id: 'versionless' } ]; + const expected = [ {id: 'versionless'} ]; assert.deepStrictEqual(res, expected); check_collection([ ], done); }); }); it('specified version', (done) => { - utils.stream_test(request({ id: 'versionless', value: 4, [hz_v]: 5 }), (err, res) => { + utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ { error: invalidated_msg } ]; + const expected = [ {error: invalidated_msg} ]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -462,9 +462,9 @@ const all_tests = (collection) => { }, done)); const writes = [ - { id: 0, a: 1 }, - { id: 0, b: 2 }, - { id: 0, c: 3 }, + {id: 0, a: 1}, + {id: 0, b: 2}, + {id: 0, c: 3}, ]; const by_version = (a, b) => a[hz_v] - b[hz_v]; @@ -472,9 +472,9 @@ const all_tests = (collection) => { const latest_index = res.findIndex((x) => x[hz_v] === 2); assert(latest_index !== -1); res.sort(by_version); - assert.deepStrictEqual(res, [ { id: 0, [hz_v]: 0 }, - { id: 0, [hz_v]: 1 }, - { id: 0, [hz_v]: 2 } ]); + assert.deepStrictEqual(res, [ {id: 0, [hz_v]: 0}, + {id: 0, [hz_v]: 1}, + {id: 0, [hz_v]: 2} ]); return writes[latest_index]; }; @@ -486,23 +486,23 @@ const all_tests = (collection) => { assert(success_index !== -1); for (let i = 0; i < res.length; ++i) { if (i === success_index) { - assert.deepStrictEqual(res[i], { id: 0, [hz_v]: 0 }); + assert.deepStrictEqual(res[i], {id: 0, [hz_v]: 0}); } else { - assert.deepStrictEqual(res[i], { error }); + assert.deepStrictEqual(res[i], {error}); } } return writes[success_index]; }; describe('Existing Row', () => { - const test_data = [ { id: 0, value: 0 } ]; + const test_data = [ {id: 0, value: 0} ]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); it('Store', (done) => { utils.stream_test(make_request('store', writes), (err, res) => { assert.ifError(err); const latest_write = check_and_get_latest_write(res); - check_collection([ Object.assign({ [hz_v]: 2 }, latest_write) ], done); + check_collection([ Object.assign({[hz_v]: 2}, latest_write) ], done); }); }); @@ -510,7 +510,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('replace', writes), (err, res) => { assert.ifError(err); const latest_write = check_and_get_latest_write(res); - check_collection([ Object.assign({ [hz_v]: 2 }, latest_write) ], done); + check_collection([ Object.assign({[hz_v]: 2}, latest_write) ], done); }); }); @@ -518,7 +518,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('upsert', writes), (err, res) => { assert.ifError(err); check_and_get_latest_write(res); - check_collection([ { id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2 } ], done); + check_collection([ {id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2} ], done); }); }); @@ -526,7 +526,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('update', writes), (err, res) => { assert.ifError(err); check_and_get_latest_write(res); - check_collection([ { id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2 } ], done); + check_collection([ {id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2} ], done); }); }); @@ -545,7 +545,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('insert', writes), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'The document already exists.'); - check_collection([ Object.assign({ [hz_v]: 0 }, success_write) ], done); + check_collection([ Object.assign({[hz_v]: 0}, success_write) ], done); }); }); @@ -553,7 +553,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('store', writes), (err, res) => { assert.ifError(err); const latest_write = check_and_get_latest_write(res); - check_collection([ Object.assign({ [hz_v]: 2 }, latest_write) ], done); + check_collection([ Object.assign({[hz_v]: 2}, latest_write) ], done); }); }); @@ -562,7 +562,7 @@ const all_tests = (collection) => { assert.ifError(err); assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [ 0, 1, 2 ]); assert.deepStrictEqual(res.map((x) => x.id), [ 0, 0, 0 ]); - check_collection([ { id: 0, a: 1, b: 2, c: 3, [hz_v]: 2 } ], done); + check_collection([ {id: 0, a: 1, b: 2, c: 3, [hz_v]: 2} ], done); }); }); }); @@ -572,15 +572,15 @@ const all_tests = (collection) => { // per iteration with the database. In order to test timeouts, we use a // timeout of zero, so the other rows should immediately error. describe('Zero Timeout', () => { - const timeout = { timeout: 0 }; - const test_data = [ { id: 0, value: 0 } ]; + const timeout = {timeout: 0}; + const test_data = [ {id: 0, value: 0} ]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); it('Store', (done) => { utils.stream_test(make_request('store', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({ [hz_v]: 0 }, success_write) ], done); + check_collection([ Object.assign({[hz_v]: 0}, success_write) ], done); }); }); @@ -588,7 +588,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('replace', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({ [hz_v]: 0 }, success_write) ], done); + check_collection([ Object.assign({[hz_v]: 0}, success_write) ], done); }); }); @@ -596,7 +596,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('upsert', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({ [hz_v]: 0 }, test_data[0], success_write) ], done); + check_collection([ Object.assign({[hz_v]: 0}, test_data[0], success_write) ], done); }); }); @@ -604,7 +604,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('update', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({ [hz_v]: 0 }, test_data[0], success_write) ], done); + check_collection([ Object.assign({[hz_v]: 0}, test_data[0], success_write) ], done); }); }); }); @@ -613,4 +613,4 @@ const all_tests = (collection) => { const suite = (collection) => describe('Write', () => all_tests(collection)); -module.exports = { suite }; +module.exports = {suite}; diff --git a/test/serve.js b/test/serve.js index b93364da4..a141bc013 100755 --- a/test/serve.js +++ b/test/serve.js @@ -1,5 +1,7 @@ #!/usr/bin/env node -'use strict' +'use strict'; + +require('../server/node_modules/source-map-support').install(); Error.stackTraceLimit = Infinity; From 25b91b637f3c509f1caae9895746f282ef4f326c Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Wed, 17 Aug 2016 00:10:07 +0000 Subject: [PATCH 15/86] checkpoint --- server/package.json | 2 +- server/{ => src}/test/http_tests.js | 0 server/{ => src}/test/permissions.js | 0 server/{ => src}/test/prereq_tests.js | 0 server/{ => src}/test/protocol_tests.js | 0 server/{ => src}/test/query_tests.js | 0 server/{ => src}/test/schema.js | 0 server/{ => src}/test/subscribe_tests.js | 0 server/{ => src}/test/test.js | 2 ++ server/{ => src}/test/utils.js | 0 server/{ => src}/test/write_tests.js | 0 11 files changed, 3 insertions(+), 1 deletion(-) rename server/{ => src}/test/http_tests.js (100%) rename server/{ => src}/test/permissions.js (100%) rename server/{ => src}/test/prereq_tests.js (100%) rename server/{ => src}/test/protocol_tests.js (100%) rename server/{ => src}/test/query_tests.js (100%) rename server/{ => src}/test/schema.js (100%) rename server/{ => src}/test/subscribe_tests.js (100%) rename server/{ => src}/test/test.js (96%) rename server/{ => src}/test/utils.js (100%) rename server/{ => src}/test/write_tests.js (100%) diff --git a/server/package.json b/server/package.json index 4eb25ea57..ae777a147 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "dist/horizon.js", "scripts": { "lint": "eslint src test", - "test": "mocha test/test.js test/schema.js test/permissions.js --timeout 10000", + "test": "mocha dist/test --timeout 10000", "coverage": "istanbul cover _mocha test/test.js", "build": "babel src -d dist -s true" }, diff --git a/server/test/http_tests.js b/server/src/test/http_tests.js similarity index 100% rename from server/test/http_tests.js rename to server/src/test/http_tests.js diff --git a/server/test/permissions.js b/server/src/test/permissions.js similarity index 100% rename from server/test/permissions.js rename to server/src/test/permissions.js diff --git a/server/test/prereq_tests.js b/server/src/test/prereq_tests.js similarity index 100% rename from server/test/prereq_tests.js rename to server/src/test/prereq_tests.js diff --git a/server/test/protocol_tests.js b/server/src/test/protocol_tests.js similarity index 100% rename from server/test/protocol_tests.js rename to server/src/test/protocol_tests.js diff --git a/server/test/query_tests.js b/server/src/test/query_tests.js similarity index 100% rename from server/test/query_tests.js rename to server/src/test/query_tests.js diff --git a/server/test/schema.js b/server/src/test/schema.js similarity index 100% rename from server/test/schema.js rename to server/src/test/schema.js diff --git a/server/test/subscribe_tests.js b/server/src/test/subscribe_tests.js similarity index 100% rename from server/test/subscribe_tests.js rename to server/src/test/subscribe_tests.js diff --git a/server/test/test.js b/server/src/test/test.js similarity index 96% rename from server/test/test.js rename to server/src/test/test.js index 8f120958b..527ed8195 100644 --- a/server/test/test.js +++ b/server/src/test/test.js @@ -1,5 +1,7 @@ 'use strict'; +require('source-map-support').install(); + const logger = require('../src/logger'); const utils = require('./utils'); diff --git a/server/test/utils.js b/server/src/test/utils.js similarity index 100% rename from server/test/utils.js rename to server/src/test/utils.js diff --git a/server/test/write_tests.js b/server/src/test/write_tests.js similarity index 100% rename from server/test/write_tests.js rename to server/src/test/write_tests.js From c9136c76c3806b815c4565042a8313bae354916a Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Wed, 17 Aug 2016 00:48:36 +0000 Subject: [PATCH 16/86] checkpoint --- server/src/reliable.js | 2 + server/src/server.js | 3 +- server/src/test/http_tests.js | 82 ---------------------------------- server/src/test/permissions.js | 2 +- server/src/test/schema.js | 2 +- server/src/test/test.js | 5 +-- server/src/test/utils.js | 27 +++++++---- server/src/test/write_tests.js | 2 +- 8 files changed, 27 insertions(+), 98 deletions(-) delete mode 100644 server/src/test/http_tests.js diff --git a/server/src/reliable.js b/server/src/reliable.js index 22c56ae62..1438a1888 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -6,6 +6,7 @@ const r = require('rethinkdb'); class Reliable { constructor(initialCbs) { + console.log(`${this.constructor} CONSTRUCTING`); this.subs = {}; this.ready = false; this.closed = false; @@ -45,6 +46,7 @@ class Reliable { // `onReady` or `onUnready`s in a row (or even just returning // early if we would). if (eventType === 'onReady') { + console.log(`${this.constructor} READY`); this.ready = arguments; } else if (eventType === 'onUnready') { this.ready = false; diff --git a/server/src/server.js b/server/src/server.js index f1e1d364d..a9f091c76 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -47,6 +47,7 @@ class Server extends EventEmitter { }); this._clients = new Set(); + // TODO: consider emitting errors sometimes. this._reliable_metadata = new ReliableMetadata( opts.project_name, this._reliable_conn, @@ -58,7 +59,7 @@ class Server extends EventEmitter { this.emit('ready', this); }, onUnready: (err) => { - this.emit('unready', this); + this.emit('unready', this, err); const msg = (err && err.message) || 'Connection became unready.'; this._clients.forEach((client) => client.close({error: msg})); this._clients.clear(); diff --git a/server/src/test/http_tests.js b/server/src/test/http_tests.js deleted file mode 100644 index dd17b8b0a..000000000 --- a/server/src/test/http_tests.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -const horizon = require('../'); - -const assert = require('assert'); -const child_process = require('child_process'); -const fs = require('fs'); -const http = require('http'); -const https = require('https'); -const path = require('path'); - -const all_tests = () => { - [ 'http', 'https' ].forEach((transport) => { - describe(transport, () => { - let http_server, key_file, cert_file; - - before('Generate key and cert', (done) => { - if (transport === 'http') { done(); return; } - - key_file = `key.${process.pid}.pem`; - cert_file = `cert.${process.pid}.pem`; - - child_process.exec( - `openssl req -x509 -nodes -batch -newkey rsa:2048 -keyout ${key_file} -days 1`, - (err, stdout) => { - assert.ifError(err); - const cert_start = stdout.indexOf('-----BEGIN CERTIFICATE-----'); - const cert_end = stdout.indexOf('-----END CERTIFICATE-----'); - assert(cert_start !== -1 && cert_end !== -1); - - const cert = `${stdout.slice(cert_start, cert_end)}-----END CERTIFICATE-----\n`; - fs.writeFile(cert_file, cert, done); - }); - }); - - after('Remove key and cert', () => { - [ key_file, cert_file ].forEach((f) => { if (f) { fs.unlinkSync(f); } }); - }); - - before('Start horizon server', (done) => { - const four_o_four = (req, res) => { - res.writeHeader(404); - res.end(); - }; - - if (transport === 'http') { - http_server = new http.createServer(four_o_four); - } else { - http_server = new https.createServer({key: fs.readFileSync(key_file), - cert: fs.readFileSync(cert_file)}, - four_o_four); - } - - horizon(http_server, {auth: {token_secret: 'hunter2'}}); - - http_server.listen(0, done); - }); - - after('Shutdown standalone horizon server', () => { - http_server.close(); - }); - - it('localhost/horizon/horizon.js', (done) => { - require(transport).get({host: 'localhost', - port: http_server.address().port, - path: '/horizon/horizon.js', - rejectUnauthorized: false}, (res) => { - const client_js = path.resolve(__dirname, '../node_modules/@horizon/client/dist/horizon.js'); - const code = fs.readFileSync(client_js); - let buffer = ''; - assert.strictEqual(res.statusCode, 200); - res.on('data', (delta) => { buffer += delta; }); - res.on('end', () => (assert.equal(buffer, code), done())); - }); - }); - }); - }); -}; - -const suite = (collection) => describe('Webserver', () => all_tests(collection)); - -module.exports = {suite}; diff --git a/server/src/test/permissions.js b/server/src/test/permissions.js index 8d9b9876c..3fc76e5f3 100644 --- a/server/src/test/permissions.js +++ b/server/src/test/permissions.js @@ -1,6 +1,6 @@ 'use strict'; -const Rule = require('../src/permissions/rule').Rule; +const Rule = require('../permissions/rule').Rule; const assert = require('assert'); diff --git a/server/src/test/schema.js b/server/src/test/schema.js index d8b908cc5..c3ee6e43f 100644 --- a/server/src/test/schema.js +++ b/server/src/test/schema.js @@ -1,6 +1,6 @@ 'use strict'; -const horizon_protocol = require('../src/schema/horizon_protocol'); +const horizon_protocol = require('../schema/horizon_protocol'); const utils = require('./utils'); const assert = require('assert'); diff --git a/server/src/test/test.js b/server/src/test/test.js index 527ed8195..ddf1a740c 100644 --- a/server/src/test/test.js +++ b/server/src/test/test.js @@ -2,11 +2,10 @@ require('source-map-support').install(); -const logger = require('../src/logger'); +const logger = require('../logger'); const utils = require('./utils'); -const all_suites = [ 'http_tests', - 'prereq_tests', +const all_suites = [ 'prereq_tests', 'protocol_tests', 'query_tests', 'subscribe_tests', diff --git a/server/src/test/utils.js b/server/src/test/utils.js index df436b8b0..40ad604e1 100644 --- a/server/src/test/utils.js +++ b/server/src/test/utils.js @@ -1,11 +1,11 @@ 'use strict'; -const horizon = require('../src/server'); -const logger = require('../src/logger'); +const horizon = require('../server'); +const logger = require('../logger'); -const rm_sync_recursive = require('../../cli/src/utils/rm_sync_recursive'); -const start_rdb_server = require('../../cli/src/utils/start_rdb_server'); -const each_line_in_pipe = require('../../cli/src/utils/each_line_in_pipe'); +const rm_sync_recursive = require('../../../cli/src/utils/rm_sync_recursive'); +const start_rdb_server = require('../../../cli/src/utils/start_rdb_server'); +const each_line_in_pipe = require('../../../cli/src/utils/each_line_in_pipe'); const assert = require('assert'); const http = require('http'); @@ -136,15 +136,24 @@ const start_horizon_server = (done) => { allow_unauthenticated: true, }, }); - horizon_server.ready().catch((err) => logger.info(`horizon server error: ${err}`)); - horizon_server.ready().then(() => logger.info('horizon server ready')); - horizon_server.ready().then(() => done()); + + horizon_server.on('ready', (server) => { + logger.info('horizon server ready'); + done(); + }); + horizon_server.on('unready', (server, err) => { + logger.info(`horizon server unready: ${err}`); + }); }); http_server.on('error', (err) => done(err)); }; const close_horizon_server = () => { - if (horizon_server !== undefined) { horizon_server.close(); } + if (horizon_server !== undefined) { + horizon_server.removeAllListeners('ready'); + horizon_server.removeAllListeners('unready'); + horizon_server.close(); + } horizon_server = undefined; }; diff --git a/server/src/test/write_tests.js b/server/src/test/write_tests.js index ae2d4b040..4d40e84c4 100644 --- a/server/src/test/write_tests.js +++ b/server/src/test/write_tests.js @@ -1,7 +1,7 @@ 'use strict'; const utils = require('./utils'); -const horizon_writes = require('../src/endpoint/writes'); +const horizon_writes = require('../endpoint/writes'); const assert = require('assert'); const crypto = require('crypto'); From 9425197d57c085d2e3f97e89b53d7841541db385 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 16 Aug 2016 19:59:06 -0700 Subject: [PATCH 17/86] fixing test bugs --- server/src/metadata/reliable_metadata.js | 32 ++++++++---- server/src/reliable.js | 63 +++++++++++++++--------- server/src/server.js | 6 +-- 3 files changed, 66 insertions(+), 35 deletions(-) diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index b67ff800b..af6e0c780 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -122,7 +122,7 @@ class ReliableInit extends Reliable { .do((res) => r.branch(res('new_val').eq(null), r.error(res('error')), - res('new_val'))).run(this._conn), + res('new_val'))).run(conn), r.db(this._db).table('hz_groups').get('admin') .replace((old_row) => r.branch(old_row.eq(null), @@ -136,7 +136,7 @@ class ReliableInit extends Reliable { .do((res) => r.branch(res('new_val').eq(null), r.error(res('error')), - res('new_val'))).run(this._conn), + res('new_val'))).run(conn), ]); }).then(() => { this.check_attempt(attempt); @@ -173,6 +173,15 @@ class ReliableMetadata extends Reliable { this._reliable_init = new ReliableInit( this._db, reliable_conn, auto_create_collection); + this._conn_subscription = this._reliable_conn.subscribe({ + onReady: (conn) => { + this._connection = conn; + }, + onUnready: () => { + this._connection = null; + }, + }); + this._collection_changefeed = new ReliableChangefeed( r.db(this._db) .table('hz_collections') @@ -231,6 +240,7 @@ class ReliableMetadata extends Reliable { reliable_conn, { onChange: (change) => { + if (!this._connection) { return; } switch (change.type) { case 'initial': case 'add': @@ -244,7 +254,7 @@ class ReliableMetadata extends Reliable { collection = new Collection(this._db, collection_name); this._collections.set(collection_name, collection); } - collection._update_table(table_id, change.new_val.indexes, this._reliable_conn.connection()); + collection._update_table(table_id, change.new_val.indexes, this._connection); } break; case 'uninitial': @@ -252,7 +262,7 @@ class ReliableMetadata extends Reliable { { const collection = this._collections.get(change.old_val.name); if (collection) { - collection._update_table(change.old_val.id, null, this._reliable_conn.connection()); + collection._update_table(change.old_val.id, null, this._connection); if (collection._is_safe_to_remove()) { this._collections.delete(collection); collection._close(); @@ -277,6 +287,7 @@ class ReliableMetadata extends Reliable { }, onUnready: () => { // TODO: fill in the reason for `close`. + this.emit('onUnready'); this._collections.forEach((collection) => collection.close()); this._collections.clear(); }, @@ -284,12 +295,13 @@ class ReliableMetadata extends Reliable { } close(reason) { - super.close(reason); - - this._reliable_init.close(reason); - this._collection_changefeed.close(reason); - this._index_changefeed.close(reason); - this._ready_union.close(reason); + return Promise.all([ + super.close(reason), + this._reliable_init.close(reason), + this._collection_changefeed.close(reason), + this._index_changefeed.close(reason), + this._ready_union.close(reason), + ]); } // Public interface for use by plugins or other classes diff --git a/server/src/reliable.js b/server/src/reliable.js index 1438a1888..69dfff3b9 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -4,10 +4,11 @@ const logger = require('./logger'); const r = require('rethinkdb'); +const subs = Symbol('subs'); + class Reliable { constructor(initialCbs) { - console.log(`${this.constructor} CONSTRUCTING`); - this.subs = {}; + this[subs] = {}; this.ready = false; this.closed = false; if (initialCbs) { @@ -20,18 +21,18 @@ class Reliable { throw new Error('Cannot subscribe to a closed ReliableConn.'); } const subId = Symbol(); - this.subs[subId] = { + this[subs][subId] = { cbs: cbs, - close: () => delete this.subs[subId], + close: () => delete this[subs][subId], }; if (this.ready && cbs.onReady) { try { cbs.onReady.apply(cbs, this.ready); } catch (e) { - // log e + logger.error(`Unexpected error in reliable callback, event: subscribe onReady, error: ${e.stack}`); } } - return this.subs[subId]; + return this[subs][subId]; } emit() { @@ -40,24 +41,24 @@ class Reliable { } } - emitInternal() { - const eventType = arguments.shift(); + emitInternal(eventType, ...args) { // TODO: consider checking to make sure we don't send two // `onReady` or `onUnready`s in a row (or even just returning // early if we would). if (eventType === 'onReady') { - console.log(`${this.constructor} READY`); - this.ready = arguments; + this.ready = args; } else if (eventType === 'onUnready') { this.ready = false; } - for (const s of Object.getOwnPropertySymbols(this.subs)) { + for (const s of Object.getOwnPropertySymbols(this[subs])) { try { - const cbs = this.subs[s].cbs; + const cbs = this[subs][s].cbs; const event = cbs[eventType]; - if (event) { event.apply(cbs, arguments); } + if (event) { + event.apply(cbs, args); + } } catch (e) { - // TODO: log e + logger.error(`Unexpected error in reliable callback, event: ${eventType}, error: ${e.stack}`); } } } @@ -67,7 +68,7 @@ class Reliable { if (this.ready) { this.emitInternal('onUnready', new Error(`closed: ${reason}`)); } - this.subs = {}; // Just to make sure no subclasses do anything clever. + this[subs] = {}; // Just to make sure no subclasses do anything clever. return Promise.resolve(); } } @@ -97,7 +98,7 @@ class ReliableConn extends Reliable { } }).catch((e) => { if (this.conn) { - logger.error(`Error in ${JSON.stringify(this)}: ${e.stack}`); + logger.error(`Error in ReliableConnection ${JSON.stringify(this.connOpts)}: ${e.stack}`); } if (!this.closed) { setTimeout(() => this.connect(), 1000); @@ -120,10 +121,18 @@ class ReliableChangefeed extends Reliable { this.reql = reql; this.reliableConn = reliableConn; - // RSI: restart this if there's an error on the cfeed rather than the connection. - this.subscription = reliableConn.subscribe({ + this.make_subscription(); + } + + make_subscription() { + if (this.closed) { return; } + this.subscription = this.reliableConn.subscribe({ onReady: (conn) => { - reql.run(conn, {includeTypes: true, includeStates: true}).then((cursor) => { + this.reql.run(conn, {includeTypes: true, includeStates: true}).then((cursor) => { + if (this.closed) { + cursor.close(); + return; + } this.cursor = cursor; return cursor.eachAsync((change) => { switch (change.type) { @@ -138,9 +147,16 @@ class ReliableChangefeed extends Reliable { }).then((res) => { // If we get here the cursor closed for some reason. throw new Error(`cursor closed for some reason: ${res}`); - }).catch((e) => { - this.emit('onUnready', e); }); + }).catch((e) => { + if (this.ready) { + this.emit('onUnready', e); + } + if (this.subscription) { + this.subscription.close(); + this.subscription = null; + setTimeout(() => this.make_subscription(), 1000); + } }); }, }); @@ -151,7 +167,10 @@ class ReliableChangefeed extends Reliable { if (this.cursor) { retProm = Promise.all([ retProm, this.cursor.close() ]); } - this.subscription.close(); + if (this.subscription) { + this.subscription.close(); + this.subscription = null; + } return retProm; } } diff --git a/server/src/server.js b/server/src/server.js index a9f091c76..ae5254f25 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -38,8 +38,8 @@ class Server extends EventEmitter { this._run_middleware = this._default_run_middleware; this._reliable_conn = new ReliableConn({ - host: opts.rbd_host, - port: opts.rdb_pot, + host: opts.rdb_host, + port: opts.rdb_port, db: opts.project_name, user: opts.rdb_user || 'admin', password: opts.rdb_password || '', @@ -70,7 +70,7 @@ class Server extends EventEmitter { const verifyClient = (info, cb) => { // Reject connections if we aren't synced with the database - if (!this._reliable_metadata.isReady()) { + if (!this._reliable_metadata.ready) { cb(false, 503, 'Connection to the database is down.'); } else { cb(true); From 7fdb2f2e249a360feee01e49094576ed977ce744 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 16 Aug 2016 20:05:54 -0700 Subject: [PATCH 18/86] fixed more test problems --- server/src/client.js | 8 ++++---- server/src/metadata/collection.js | 2 +- server/src/metadata/reliable_metadata.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/client.js b/server/src/client.js index b3d527cd0..91c337d22 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -10,6 +10,10 @@ const websocket = require('ws'); class Client { constructor(socket, auth, reliable_metadata, auth_middleware_cb, request_middleware_cb) { + if (!reliable_metadata.ready) { + throw new Error('No connection to the database.'); + } + logger.debug('Client connection established.'); this.socket = socket; this.auth = auth; @@ -29,10 +33,6 @@ class Client { // The first message should always be the handshake this.socket.once('message', (data) => this.error_wrap_socket(() => this.handle_handshake(data))); - - if (!this.metadata.ready) { - throw new Error('No connection to the database.'); - } } handle_websocket_close() { diff --git a/server/src/metadata/collection.js b/server/src/metadata/collection.js index 422198741..b9417edfa 100644 --- a/server/src/metadata/collection.js +++ b/server/src/metadata/collection.js @@ -14,7 +14,7 @@ class Collection { this._waiters = [ ]; } - _close() { + close() { this._tables.forEach((table) => { table._waiters.forEach((w) => w(new Error('collection deleted'))); table._waiters = [ ]; diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index af6e0c780..7b5f12c7b 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -265,7 +265,7 @@ class ReliableMetadata extends Reliable { collection._update_table(change.old_val.id, null, this._connection); if (collection._is_safe_to_remove()) { this._collections.delete(collection); - collection._close(); + collection.close(); } } } @@ -339,7 +339,7 @@ class ReliableMetadata extends Reliable { }).catch((err) => { if (collection._is_safe_to_remove()) { this._collections.delete(name); - collection._close(); + collection.close(); } done(err); }); From 757fc0741eb7cc516f8f7a93ab1fd0c25cac712a Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 16 Aug 2016 20:07:30 -0700 Subject: [PATCH 19/86] fixed eslint problems --- server/src/server.js | 4 ++-- server/src/test/utils.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/server.js b/server/src/server.js index ae5254f25..39c403ad2 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -119,8 +119,8 @@ class Server extends EventEmitter { return this._reliable_conn; } - add_middleware(mw) { - this._middlewares.push(mw); + add_middleware(new_mw) { + this._middlewares.push(new_mw); this._run_middleware = this._default_run_middleware; for (let i = this._middlewares.length - 1; i >= 0; --i) { const mw = this._middlewares[i]; diff --git a/server/src/test/utils.js b/server/src/test/utils.js index 40ad604e1..5e787fc3e 100644 --- a/server/src/test/utils.js +++ b/server/src/test/utils.js @@ -137,7 +137,7 @@ const start_horizon_server = (done) => { }, }); - horizon_server.on('ready', (server) => { + horizon_server.on('ready', () => { logger.info('horizon server ready'); done(); }); From ea26fec15a927a442c876b37a51de99335a0e590 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Thu, 18 Aug 2016 01:55:33 +0000 Subject: [PATCH 20/86] wrote broken graph code --- examples/health-check-plugin/health-check.js | 10 +- plugin_router/.eslintrc.js | 16 +++ plugin_router/package.json | 6 +- plugin_router/src/index.js | 125 ++++++++++++++----- 4 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 plugin_router/.eslintrc.js diff --git a/examples/health-check-plugin/health-check.js b/examples/health-check-plugin/health-check.js index b8661ce7a..966d62248 100644 --- a/examples/health-check-plugin/health-check.js +++ b/examples/health-check-plugin/health-check.js @@ -17,9 +17,13 @@ module.exports = (config) => { res.send("healthy"); }, methods: { - 'healthCheck': (req, res, next) => { - console.log(`healthCheck method: ${[req, res, next]}`) - res.send("healthy"); + 'healthCheck': { + requires: ['hz_permissions'], + type: 'terminal', // or `middleware` or `preReq` + impl: (req, res, next) => { + console.log(`healthCheck method: ${[req, res, next]}`) + res.send("healthy"); + }, }, }, middleware: (req, res, next) => { diff --git a/plugin_router/.eslintrc.js b/plugin_router/.eslintrc.js new file mode 100644 index 000000000..535101a8a --- /dev/null +++ b/plugin_router/.eslintrc.js @@ -0,0 +1,16 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "max-len": [ ERROR, 130 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugin_router/package.json b/plugin_router/package.json index 720bcceb8..e3cc87d8c 100644 --- a/plugin_router/package.json +++ b/plugin_router/package.json @@ -5,7 +5,7 @@ "main": "src/index.js", "scripts": { "lint": "eslint src test", - "test": "mocha test", + "test": "mocha test" }, "repository": { "type": "git", @@ -16,7 +16,7 @@ "bugs": { "url": "https://github.com/rethinkdb/horizon/issues" }, - "homepage": "https://github.com/rethinkdb/horizon#readme" + "homepage": "https://github.com/rethinkdb/horizon#readme", "engines": { "node": ">=4.0.0" }, @@ -25,6 +25,6 @@ "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", + "mocha": "^2.3.3" } } diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 9ccad83d3..4b4c734a0 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -1,19 +1,5 @@ 'use strict'; -const examplePluginActivateResult = { - name: 'graphql', - deactivate: () => { }, - - httpRoute: (req, res, next) => { }, - commands: { - 'repair': ..., - } - methods: { - 'insert': ..., - 'delete': ..., - }, -} - class PluginRouter { constructor(server) { this.server = server @@ -43,6 +29,7 @@ class PluginRouter { this.httpRoutes[plugin.name] = active; for (const m in active.methods) { this.methods[m] = active.methods[m]; + this._requiresOrdering = null; } }); return this.plugins[plugin.name]; @@ -52,11 +39,62 @@ class PluginRouter { if (!this.plugins[plugin.name]) { return Promise.reject(new Error(`Plugin '${plugin.name}' is not present.`)); } - return this.plugins[plugin.name].then(() => { + return this.plugins[plugin.name].then((active) => { + for (const m in active.methods) { + delete this.methods[m]; + this._requiresOrdering = null; + } if (plugin.deactivate) { plugin.deactivate(reason || "Removed from PluginRouter."); } + }); + } + + requiresOrdering() { + if (!this._requiresOrdering) { + this._requiresOrdering = {}; + + // RSI: use tsort instead of doing this ourselves like mega chumps. + const graph = {}; + for (const m in this.methods) { + if (!graph[m]) { + graph[m] = {name: m, inDegree: 0, children: {}}; + } + for (const r in this.methods[m].requires) { + graph[m].inDegree += 1; + if (!graph[r]) { + // RSI: assert that `r` is in `this.methods`. + graph[r] = {name: m, inDegree: 0, children: {}}; + } + graph[r].children[m] = true; + } + } + + const order = []; + const heap = new Heap((a, b) => a.inDegree - b.inDegree); + for (const g in graph) { + heap.push(graph[g]); + } + while (heap.size() > 0) { + const minItem = heap.pop(); + if (minItem.inDegree != 0) { + // ERROR: cycle (!!!) + } + for (const c in minItem.children) { + if (graph[c].inDegree <= 0) { + // ERROR: algorithm mistake + } + graph[c].inDegree -= 1; + heap.updateItem(graph[c]); + } + order.push(minItem.name) + } + + for (const i in order) { + this._requiresOrdering[order[i]] = i; + } } + return this._requiresOrdering; } httpMiddleware() { @@ -74,30 +112,53 @@ class PluginRouter { hzMiddleware() { return (req, res, next) => { - const method = (req.type && this.methods[req.type]) || next; - let cb = method; + let terminalName = null; + const requirements = {}; if (req.options) { for (const o in req.options) { - if (o !== req.type) { - const m = this.methods[o]; - if (m) { - const old_cb = cb; - cb = (maybeErr) => { - if (maybeErr instanceof Error) { - next(maybeErr); - } else { - try { - m(req, res, old_cb); - } catch (e) { - next(e); - } - } + const m = this.methods[o]; + if (m) { + if (m.type == 'terminal') { + if (terminalName !== null) { + next(new Error('multiple terminals in request: ' + + `${terminalName}, ${o}`)); + } else { + terminalName = o; } + } else { + requirements[o] = true; + } + for (const r of m.requires) { + requirements[r] = true; } } } } - cb(); + + if (terminalName === null) { + next(new Error('no terminal in request')); + } else if (requirements[terminalName]) { + next(new Error('terminal ${terminalName} is also a requirement')); + } else { + const ordering = requiresOrdering(); + const middlewareChain = Object.keys(requirements).sort( + (a, b) => ordering[a] - ordering[b]); + middlewareChain.push(terminalName); + + middlewareChain.reduceRight((cb, methodName) => { + return (maybeErr) => { + if (maybeErr instanceof Error) { + next(maybeErr); + } else { + try { + this.methods[methodName].impl(req, res, cb) + } catch (e) { + next(e); + } + } + } + }, next)(); + } } } } From 1432a9e8a492bacc92ae459b2f2203dedac3914c Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 17 Aug 2016 20:25:52 -0700 Subject: [PATCH 21/86] fixed eslint errors in plugin router --- plugin_router/src/index.js | 41 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 4b4c734a0..395f02c3c 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -2,27 +2,27 @@ class PluginRouter { constructor(server) { - this.server = server - this.plugins = {} - this.httpRoutes = {} - this.methods = {} + this.server = server; + this.plugins = {}; + this.httpRoutes = {}; + this.methods = {}; } add(plugin) { if (this.plugins[plugin.name]) { return Promise.reject( - new Error(`Plugin conflict: '${plugin.name}' already present.`)); + new Error(`Plugin conflict: "${plugin.name}" already present.`)); } - const activePlugin = Promise.resolve(server.ctx()).then(plugin.activate) + const activePlugin = Promise.resolve(this.server.ctx()).then(plugin.activate); this.plugins[plugin.name] = activePlugin.then((active) => { if (this.httpRoutes[plugin.name]) { - throw new Error(`Plugin conflict: '${plugin.name}' already present.`); + throw new Error(`Plugin conflict: "${plugin.name}" already present.`); } // RSI: validate method name is a legal identifier and doesn't // conflict with our methods. for (const m in active.methods) { if (this.methods[m]) { - throw new Error(`Method name conflict: '${m}'); + throw new Error(`Method name conflict: "${m}"`); } } @@ -37,7 +37,7 @@ class PluginRouter { remove(plugin, reason) { if (!this.plugins[plugin.name]) { - return Promise.reject(new Error(`Plugin '${plugin.name}' is not present.`)); + return Promise.reject(new Error(`Plugin "${plugin.name}" is not present.`)); } return this.plugins[plugin.name].then((active) => { for (const m in active.methods) { @@ -45,7 +45,7 @@ class PluginRouter { this._requiresOrdering = null; } if (plugin.deactivate) { - plugin.deactivate(reason || "Removed from PluginRouter."); + plugin.deactivate(reason || 'Removed from PluginRouter.'); } }); } @@ -77,7 +77,7 @@ class PluginRouter { } while (heap.size() > 0) { const minItem = heap.pop(); - if (minItem.inDegree != 0) { + if (minItem.inDegree !== 0) { // ERROR: cycle (!!!) } for (const c in minItem.children) { @@ -87,7 +87,7 @@ class PluginRouter { graph[c].inDegree -= 1; heap.updateItem(graph[c]); } - order.push(minItem.name) + order.push(minItem.name); } for (const i in order) { @@ -107,7 +107,7 @@ class PluginRouter { } else { next(); } - } + }; } hzMiddleware() { @@ -118,7 +118,7 @@ class PluginRouter { for (const o in req.options) { const m = this.methods[o]; if (m) { - if (m.type == 'terminal') { + if (m.type === 'terminal') { if (terminalName !== null) { next(new Error('multiple terminals in request: ' + `${terminalName}, ${o}`)); @@ -140,26 +140,25 @@ class PluginRouter { } else if (requirements[terminalName]) { next(new Error('terminal ${terminalName} is also a requirement')); } else { - const ordering = requiresOrdering(); + const ordering = this.requiresOrdering(); const middlewareChain = Object.keys(requirements).sort( (a, b) => ordering[a] - ordering[b]); middlewareChain.push(terminalName); - middlewareChain.reduceRight((cb, methodName) => { - return (maybeErr) => { + middlewareChain.reduceRight((cb, methodName) => + (maybeErr) => { if (maybeErr instanceof Error) { next(maybeErr); } else { try { - this.methods[methodName].impl(req, res, cb) + this.methods[methodName].impl(req, res, cb); } catch (e) { next(e); } } - } - }, next)(); + }, next)(); } - } + }; } } From 7df9136aee6e3d532cdc4a53a6c243ec932580dd Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 18 Aug 2016 00:50:25 -0700 Subject: [PATCH 22/86] checkpoint --- plugin_router/src/index.js | 14 +++--- plugins/permissions/src/permissions.js | 64 +++++++++++++++++++++++--- server/src/server.js | 34 +++----------- 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 395f02c3c..1e7b7983c 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -60,13 +60,15 @@ class PluginRouter { if (!graph[m]) { graph[m] = {name: m, inDegree: 0, children: {}}; } - for (const r in this.methods[m].requires) { - graph[m].inDegree += 1; - if (!graph[r]) { - // RSI: assert that `r` is in `this.methods`. - graph[r] = {name: m, inDegree: 0, children: {}}; + if (this.methods[m].requires) { + for (const r in this.methods[m].requires) { + graph[m].inDegree += 1; + if (!graph[r]) { + // RSI: assert that `r` is in `this.methods`. + graph[r] = {name: m, inDegree: 0, children: {}}; + } + graph[r].children[m] = true; } - graph[r].children[m] = true; } } diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 5f7f6e399..a464d215a 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -1,20 +1,70 @@ 'use strict'; -// RSI: check connection +// auth plugins should set 'request.context.user' +// token/anonymous: this should be the user id +// unauthenticated: this should be null, and will use the default rules + module.exports = (config) => { return { name: 'permissions', - activate: (ctx) => { + activate: (server) => { + const reliable_conn = server.conn(); + const user_feeds = new Map(); + + // TODO: need to save/close the subscription? + reliable_conn.subscribe({ + onUnready: (reason) => { + user_feeds.forEach((feed) => feed.close(reason)); + user_feeds.clear(); + }, + }); + + // Set up a reliable changefeed on the 'groups' table to track rules by group name + const groups_feed = new ReliableChangefeed( + r.db(server.project_name) + .table('hz_groups') + .changes({squash: false, includeInitial: true, includeTypes: true}), + reliable_conn, + { + onReady: () => { + }, + onUnready: () => { + }, + onChange: () => { + }, + }); + ctx.logger.info('Activating permissions.'); return { - middleware: (req, res, next) => { - const currentUser = req.userFeed + deactivate: (reason) => { + user_feeds.forEach((feed) => feed.close(reason)); + user_feeds.clear(); + }, + methods: { + 'permissions': { + type: 'prereq', + run: (req, res, next) => { + const user = request.context.user; + if (user === undefined) { + throw new Error('Client has not been authenticated'); + } + + // Create a changefeed for the user unless it exists + if (user_feeds + + + // Template-match the request options + + // Add a ruleset to request.context.rules + + // On changes to the user's groups, re-evaluate rules + + // On changes to the rules in any of the user's groups, re-evaluate rules + }, + } }, }; }, - deactivate: (reason) => { - ctx.logger.info(`Deactivating health-check module (${reason}).`) - }, }; } diff --git a/server/src/server.js b/server/src/server.js index 39c403ad2..f65c80709 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -31,11 +31,10 @@ class Server extends EventEmitter { this._request_handlers = new Map(); this._ws_servers = [ ]; this._close_promise = null; - this._middlewares = [ ]; - this._default_run_middleware = (req, res, next) => { - next(new Error('No terminal middleware to handle the request.')); + this._default_middleware = (req, res, next) => { + next(new Error('No middleware to handle the request.')); }; - this._run_middleware = this._default_run_middleware; + this._middleware = this._default_middleware; this._reliable_conn = new ReliableConn({ host: opts.rdb_host, @@ -54,6 +53,7 @@ class Server extends EventEmitter { this._clients, opts.auto_create_collection, opts.auto_create_index); + this._clear_clients_subscription = this._reliable_metadata.subscribe({ onReady: () => { this.emit('ready', this); @@ -89,7 +89,7 @@ class Server extends EventEmitter { socket, this._auth, this._reliable_metadata, - (...rest) => this._run_middleware.apply(this, rest) + this._middleware ); this._clients.add(client); socket.on('close', () => this._clients.delete(client)); @@ -119,28 +119,8 @@ class Server extends EventEmitter { return this._reliable_conn; } - add_middleware(new_mw) { - this._middlewares.push(new_mw); - this._run_middleware = this._default_run_middleware; - for (let i = this._middlewares.length - 1; i >= 0; --i) { - const mw = this._middlewares[i]; - const old_cb = this._run_middleware; - this._run_middleware = (req, res, next) => { - try { - mw(req, - res, - (maybeErr) => { - if (maybeErr instanceof Error) { - next(maybeErr); - } else { - old_cb(req, res, next); - } - }); - } catch (e) { - next(e); - } - }; - } + set_middleware(mw) { + this._middleware = mw ? mw : this._default_middleware; } // TODO: We close clients in `onUnready` above, but don't wait for From 574818888a55062bdf02975527f204d6436b64dc Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 19 Aug 2016 15:39:47 -0700 Subject: [PATCH 23/86] checkpoint --- plugins/permissions/src/permissions.js | 102 ++++++++++++++++++++----- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index a464d215a..7a457dec2 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -4,18 +4,53 @@ // token/anonymous: this should be the user id // unauthenticated: this should be null, and will use the default rules + module.exports = (config) => { + class User { + constructor(user_id, reliable_conn) { + this.feed = r.db(config.project_name) + .table(config.user_table) + . + } + + group_changed(group_name) { + if (this.data && this.data.groups && this.data.groups.indexOf(group_name) !== -1) { + this.active_rulesets.forEach((ruleset) => ); + } + } + + add_request(req, res, next) { + // Create a changefeed for the user unless it exists + + // Template-match the request options + + // Add a ruleset to request.context.rules + const ruleset = new Ruleset(); + request.context.rules = ruleset; + this.active_rulesets.add(ruleset); + + const cleanup = () => this.active_rulesets.delete(ruleset); + + // On changes to the rules in any of the user's groups, re-evaluate rules + + // On response completion, stop tracking the ruleset + res.complete.then(cleanup).catch(cleanup); + } + } + return { name: 'permissions', activate: (server) => { const reliable_conn = server.conn(); - const user_feeds = new Map(); + const users = new Map(); + const groups = new Map(); + const ready = false; // TODO: need to save/close the subscription? reliable_conn.subscribe({ onUnready: (reason) => { - user_feeds.forEach((feed) => feed.close(reason)); - user_feeds.clear(); + users.forEach((user) => user.close(reason)); + users.clear(); }, }); @@ -27,13 +62,46 @@ module.exports = (config) => { reliable_conn, { onReady: () => { + ready = true; }, onUnready: () => { + ready = false; + groups.forEach((g) => g.close()); + groups.clear(); }, - onChange: () => { + onChange: (change) => { + switch(change.type) { + 'initial': + 'add': + 'change': + { + const group = new Group(change.new_val); + groups.set(group.name, group); + users.forEach((user) => user.group_changed(group.name)); + } + break; + 'uninitial': + 'remove': + { + const name = change.old_val.id; + const group = groups.delete(change.old_val.id); + } + break; + default: + // RSI: log error + break; + } }, }); + const get_user = (user_id) => { + let user = users.get(user_id); + if (!user) { + user = new User(user_id, reliable_conn); + users.set(user_id, user); + } + return user; + }; ctx.logger.info('Activating permissions.'); return { @@ -45,22 +113,18 @@ module.exports = (config) => { 'permissions': { type: 'prereq', run: (req, res, next) => { - const user = request.context.user; - if (user === undefined) { - throw new Error('Client has not been authenticated'); + if (!ready) { + throw new Error('Groups are not synced with the server, cannot validate requests.'); } - // Create a changefeed for the user unless it exists - if (user_feeds - - - // Template-match the request options - - // Add a ruleset to request.context.rules - - // On changes to the user's groups, re-evaluate rules + const user_id = request.context.user_id; + if (user_id !== undefined) { + throw new Error('Client has not been authenticated'); + } - // On changes to the rules in any of the user's groups, re-evaluate rules + // Find the user and register this request + const user = get_user(user_id); + user.add_request(req, res, next); }, } }, @@ -68,3 +132,7 @@ module.exports = (config) => { }, }; } + +module.exports.validate = (rules, context, ...args) => [ + +} From 5094ea824e0158c683047c0fe1c0b17cc03cd584 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Sat, 20 Aug 2016 07:18:32 +0000 Subject: [PATCH 24/86] checkpoint --- examples/health-check-plugin/health-check.js | 6 + plugins/permissions/src/permissions.js | 248 ++++++++++++++++++- server/src/reliable.js | 31 ++- 3 files changed, 262 insertions(+), 23 deletions(-) diff --git a/examples/health-check-plugin/health-check.js b/examples/health-check-plugin/health-check.js index 966d62248..b01853d01 100644 --- a/examples/health-check-plugin/health-check.js +++ b/examples/health-check-plugin/health-check.js @@ -26,6 +26,12 @@ module.exports = (config) => { }, }, }, + onClientEvent: { + connect: (clientCtx) => { + }, + auth: (clientCtx) => { + }, + }, middleware: (req, res, next) => { req.healthy = true; next(); diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 7a457dec2..91e2e6b2e 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -4,13 +4,241 @@ // token/anonymous: this should be the user id // unauthenticated: this should be null, and will use the default rules +addToMapSet(map, name, el) { + let set = map.get(name); + if (!set) { + set = new Set(); + map.set(name, set); + } + // RSI: This might not always be an error. + assert(!set.has(el), `addToMapSet: ${name} already has ${el}`); + set.add(el); +} + +delFromMapSet(map, name, el) { + let set = map.get(name); + assert(set, `delFromMapSet: ${name} not in map`); + assert(set.has(el), `delFromMapSet: ${name} does not have ${el}`); + set.delete(el); + if (set.size === 0) { + map.delete(name); + } +} + +getMapSet(map, name) { + return map.get(name) || new Set(); +} + +class RuleMap { + constructor() { + this.groupToRulenames = new Map(); + this.RulenameToRule = new Map(); + this.groupToUsers = new Map(); + + this.userToRulenames = new Map(); // computed + // RSI: pick up here, update this in the functions below. + this.userToTimestamps = new Map(); // updated when user's rules change + } + + addUserGroup(user, group) { + addToMapSet(this.groupToUsers, group, user); + getMapSet(this.groupToRulenames, group).forEach((rn) => { + addToMapSet(this.userToRulenames, user, rn); + }); + } + + delUserGroup(user, group) { + delFromMapSet(this.groupToUsers, group, user); + getMapSet(this.groupToRulenames, group).forEach((rn) => { + delFromMapSet(this.userToRulenames, user, rn); + }); + } + + addGroupRule(group, ruleName, rule) { + this.RulenameToRule.set(ruleName, rule); + addToMapSet(this.groupToRulenames, group, ruleName); + getMapSet(this.groupToUsers, group).forEach((user) => { + addToMapSet(this.userToRulenames, user, ruleName); + }); + } + + delGroupRule(group, ruleName) { + assert(rulenameToRule.has(ruleName), `unrecognized ${group} rule ${ruleName}`); + this.rulenameToRule.delete(ruleName); + delFromMapSet(this.groupToRulenames, group, ruleName); + getMapSet(this.groupToUsers, group).forEach((user) => { + delFromMapSet(this.userToRulenames, user, ruleName); + }); + } + + // This should be equivalent to calling `delGroupRule` for all rules. + delAllGroupRules() { + this.groupToRulenames.clear(); + this.RulenameToRule.clear(); + this.userToRulenames.clear(); + } +} + +class UserCache { + constructor(config, ctx) { + this.timeout = config.cacheTimeout; + + this.ruleMap = new RuleMap(); + this.groupsUnreadyAt = new Date(0); // epoch + + this.userCfeeds = new Map(); + this.newUserCfeed = (userId) => { + let oldGroups = new Set(); + const cfeed = new ReliableChangefeed( + r.table(config.usersTable).get(userId).changes({includeInitial: true}), + ctx.reliableConn, + { + onUnready: () => { + cfeed.unreadyAt = new Date(); + }, + onChange: (change) => { + cfeed.unreadyAt = null; // We're ready on every change. + const newGroups = new Set((change.new_val && change.new_val.groups) || []); + oldGroups.forEach((g) => { + if (!newGroups.has(g)) { + this.ruleMap.delUserGroup(userId, g); + } + }); + newGroups.forEach((g) => { + if (!oldGroups.has(g)) { + this.ruleMap.addUserGroup(userId, g); + } + }); + oldGroups = newGroups; + }, + } + ); + if (cfeed.unreadyAt === undefined) { + cfeed.unreadyAt = new Date(0); // epoch + } + return cfeed; + }; + + this.groupCfeed = new ReliableChangefeed( + r.table(config.groupsTable).changes({includeInitial: true}), + ctx.reliableConn, + { + onReady: () => { + this.ruleMap.delAllGroupRules(), + this.queuedGroups.forEach( + (rules, groupId) => rules.forEach( + (rule) => this.ruleMap.addGroupRule( + groupId, JSON.stringify([groupId, rule.name]), new Rule(rule)))); + this.queuedGroups.clear(); + + assert(this.groupsUnreadyAt !== null); + this.groupsUnreadyAt = null; + }, + onUnready: () => { + assert(this.queuedGroups.size === 0); + assert(this.groupsUnreadyAt === null); + this.groupsUnreadyAt = new Date(); + }, + onChange: (change) => { + const id = change.old_val ? change.old_val.id : change.new_val.id; + if (this.groupsUnreadyAt !== null) { + queuedGroups.set(id, change.rules); + } else { + const oldRules = change.old_val ? change.old_val.rules : {}; + const newRules = change.new_val ? change.new_val.rules : {}; + for (const k in oldRules) { + if (newRules[k] && + oldRules[k].template === newRules[k].template && + oldRules[k].validator === newRules[k].validator) { + delete newRules[k]; + } else { + this.ruleMap.delGroupRule(id, k); + } + } + for (const k in newRules) { + this.ruleMap.addGroupRule(id, k, new Rule(newRules[k])); + } + } + }, + } + ); + } + + subscribe(userId) { + let cfeed = this.userCfeeds.get(userId); + if (!cfeed) { + this.userCfeeds.set(userId, cfeed = newUserCfeed()); + cfeed.readyPromise = new Promise((resolve, reject) => { + cfeed.subscribe({onReady: () => resolve()}); + setTimeout(() => reject(new Error('timed out')), this.timeout) + }); + } + + return { + getValidatePromise(req) { + return cfeed.readyPromise.then(() => { + return (...args) => + }); + }, + close() { + + }, + }; + } +} + +// RSI: remove the extra level of function calling. +module.exports = (config) => { + const name = config.name || 'permissions', + const userCache = Symbol(`${name}_userCache`); + const userSub = Symbol(`${name}_userSub`); + return { + name, + + activate(ctx) { + ctx.logger.info('Activating plugins module.'); + ctx[userCache] = new UserCache(config, ctx); + return { + methods: { + 'hz_permissions': { + type: 'preReq', + handler: (req, res, next) => { + if (!req.clientCtx[userSub]) { + next(new Error('client connection not authenticated')); + } else { + req.clientCtx[userSub].getValidatePromise(req).then((validate) => { + req.validate = validate; + next(); + }).catch(next); + } + }, + }, + }, + onClientEvent: { + auth: (clientCtx) => { + clientCtx[userSub] = ctx[userCache].subscribe(clientCtx.user.id); + }, + disconnect: (clientCtx) => { + if (clientCtx[userSub]) { + clientCtx[userSub].close(); + } + }, + }, + }; + }, + + deactivate(ctx) { + if (ctx[userCache]) { + ctx[userCache].close(); + } + }, + }; +} module.exports = (config) => { class User { constructor(user_id, reliable_conn) { - this.feed = r.db(config.project_name) - .table(config.user_table) - . + this.feed = r.table(config.user_table); } group_changed(group_name) { @@ -18,12 +246,12 @@ module.exports = (config) => { this.active_rulesets.forEach((ruleset) => ); } } - + add_request(req, res, next) { // Create a changefeed for the user unless it exists // Template-match the request options - + // Add a ruleset to request.context.rules const ruleset = new Ruleset(); request.context.rules = ruleset; @@ -45,7 +273,7 @@ module.exports = (config) => { const users = new Map(); const groups = new Map(); const ready = false; - + // TODO: need to save/close the subscription? reliable_conn.subscribe({ onUnready: (reason) => { @@ -53,7 +281,7 @@ module.exports = (config) => { users.clear(); }, }); - + // Set up a reliable changefeed on the 'groups' table to track rules by group name const groups_feed = new ReliableChangefeed( r.db(server.project_name) @@ -114,7 +342,8 @@ module.exports = (config) => { type: 'prereq', run: (req, res, next) => { if (!ready) { - throw new Error('Groups are not synced with the server, cannot validate requests.'); + throw new Error( + 'Groups are not synced with the server, cannot validate requests.'); } const user_id = request.context.user_id; @@ -133,6 +362,5 @@ module.exports = (config) => { }; } -module.exports.validate = (rules, context, ...args) => [ - +module.exports.validate = (rules, context, ...args) => { } diff --git a/server/src/reliable.js b/server/src/reliable.js index 69dfff3b9..330377eeb 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -8,7 +8,7 @@ const subs = Symbol('subs'); class Reliable { constructor(initialCbs) { - this[subs] = {}; + this[subs] = new Map(); this.ready = false; this.closed = false; if (initialCbs) { @@ -16,23 +16,28 @@ class Reliable { } } + numSubs() { + return this[subs].size; + } + subscribe(cbs) { if (this.closed) { throw new Error('Cannot subscribe to a closed ReliableConn.'); } const subId = Symbol(); - this[subs][subId] = { + this[subs].set(subId, { cbs: cbs, - close: () => delete this[subs][subId], - }; + close: () => this[subs].delete(subId), + }); if (this.ready && cbs.onReady) { try { cbs.onReady.apply(cbs, this.ready); } catch (e) { - logger.error(`Unexpected error in reliable callback, event: subscribe onReady, error: ${e.stack}`); + logger.error(`Unexpected error in reliable callback, `+ + `event: subscribe onReady, error: ${e.stack}`); } } - return this[subs][subId]; + return this[subs].get(subId); } emit() { @@ -50,17 +55,17 @@ class Reliable { } else if (eventType === 'onUnready') { this.ready = false; } - for (const s of Object.getOwnPropertySymbols(this[subs])) { + this[subs].forEach((sub) => { try { - const cbs = this[subs][s].cbs; - const event = cbs[eventType]; + const event = sub.cbs[eventType]; if (event) { - event.apply(cbs, args); + event.apply(sub.cbs, args); } } catch (e) { - logger.error(`Unexpected error in reliable callback, event: ${eventType}, error: ${e.stack}`); + logger.error(`Unexpected error in reliable callback, `+ + `event: ${eventType}, error: ${e.stack}`); } - } + }); } close(reason) { @@ -68,7 +73,7 @@ class Reliable { if (this.ready) { this.emitInternal('onUnready', new Error(`closed: ${reason}`)); } - this[subs] = {}; // Just to make sure no subclasses do anything clever. + this[subs].clear(); // Just to make sure no subclasses do anything clever. return Promise.resolve(); } } From e4b93bcba9b098cf68f2a5c9dfb2ca930096ba03 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Tue, 23 Aug 2016 18:41:15 +0000 Subject: [PATCH 25/86] fixed square brackets style --- .eslintrc.js | 2 +- server/src/auth.js | 8 +- server/src/client.js | 2 +- server/src/endpoint/query.js | 14 +-- server/src/endpoint/subscribe.js | 2 +- server/src/endpoint/update.js | 2 +- server/src/endpoint/upsert.js | 6 +- server/src/endpoint/writes.js | 6 +- server/src/metadata/collection.js | 10 +- server/src/metadata/index.js | 10 +- server/src/metadata/reliable_metadata.js | 8 +- server/src/metadata/table.js | 10 +- server/src/permissions/rule.js | 2 +- server/src/permissions/template.js | 10 +- server/src/reliable.js | 8 +- server/src/response.js | 4 +- server/src/server.js | 2 +- server/src/test/permissions.js | 84 +++++++------- server/src/test/prereq_tests.js | 4 +- server/src/test/protocol_tests.js | 6 +- server/src/test/query_tests.js | 84 +++++++------- server/src/test/schema.js | 116 +++++++++--------- server/src/test/test.js | 4 +- server/src/test/write_tests.js | 142 +++++++++++------------ server/src/utils.js | 2 +- 25 files changed, 274 insertions(+), 274 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 642ace597..4bead8d9c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { extends: "eslint:recommended", rules: { "arrow-body-style": [ERROR, "as-needed"], - "array-bracket-spacing": [ ERROR, "always" ], + "array-bracket-spacing": [ ERROR, "never" ], "arrow-parens": [ ERROR, "always" ], "arrow-spacing": [ ERROR ], "block-spacing": [ ERROR, "always" ], diff --git a/server/src/auth.js b/server/src/auth.js index 73386c029..f48793503 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -38,7 +38,7 @@ class JWT { } verify(token) { - return jwt.verifyAsync(token, this.secret, {algorithms: [ this.algorithm ]}) + return jwt.verifyAsync(token, this.secret, {algorithms: [this.algorithm]}) .then((payload) => ({token, payload})); } } @@ -82,16 +82,16 @@ class Auth { // Can't use objects in primary keys, so convert those to JSON in the db (deterministically) auth_key(provider, info) { if (info === null || Array.isArray(info) || typeof info !== 'object') { - return [ provider, info ]; + return [provider, info]; } else { - return [ provider, r.expr(info).toJSON() ]; + return [provider, r.expr(info).toJSON()]; } } new_user_row(id) { return { id, - groups: [ 'default', this._new_user_group ], + groups: ['default', this._new_user_group], [writes.version_field]: 0, }; } diff --git a/server/src/client.js b/server/src/client.js index 91c337d22..ededb4a8a 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -138,7 +138,7 @@ class Client { }); }); } else { - this.user_info.groups = [ 'default' ]; + this.user_info.groups = ['default']; finish_handshake(); } }).catch((err) => { diff --git a/server/src/endpoint/query.js b/server/src/endpoint/query.js index a02a6b742..86c324157 100644 --- a/server/src/endpoint/query.js +++ b/server/src/endpoint/query.js @@ -11,9 +11,9 @@ const object_to_fields = (obj) => Object.keys(obj).map((key) => { const value = obj[key]; if (value !== null && typeof value === 'object' && !value.$reql_type$) { - return object_to_fields(value).map((subkeys) => [ key ].concat(subkeys)); + return object_to_fields(value).map((subkeys) => [key].concat(subkeys)); } else { - return [ key ]; + return [key]; } }); @@ -30,7 +30,7 @@ const make_reql = (raw_request, metadata) => { const fuzzy_fields = object_to_fields(obj); const order_keys = (options.order && options.order[0]) || (options.above && Object.keys(options.above[0])) || - (options.below && Object.keys(options.below[0])) || [ ]; + (options.below && Object.keys(options.below[0])) || []; if (order_keys.length >= 1) { const k = order_keys[0]; @@ -45,7 +45,7 @@ const make_reql = (raw_request, metadata) => { `"${k}" cannot be used in "order", "above", or "below" when finding by that field.`); }); - const index = collection.get_matching_index(fuzzy_fields, order_keys.map((k) => [ k ])); + const index = collection.get_matching_index(fuzzy_fields, order_keys.map((k) => [k])); const get_bound = (name) => { const eval_key = (key) => { @@ -107,10 +107,10 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { done(new Error('Operation not permitted.')); cursor.close().catch(() => { }); } else { - send({data: [ item ]}); + send({data: [item]}); } }).then(() => { - done({data: [ ], state: 'complete'}); + done({data: [], state: 'complete'}); }); } else if (res !== null && res.constructor.name === 'Array') { for (const item of res) { @@ -122,7 +122,7 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { } else if (!ruleset.validate(context, res)) { done(new Error('Operation not permitted.')); } else { - done({data: [ res ], state: 'complete'}); + done({data: [res], state: 'complete'}); } }).catch(done); diff --git a/server/src/endpoint/subscribe.js b/server/src/endpoint/subscribe.js index f7af36422..06af64120 100644 --- a/server/src/endpoint/subscribe.js +++ b/server/src/endpoint/subscribe.js @@ -24,7 +24,7 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { (item.new_val && !ruleset.validate(context, item.new_val))) { throw new Error('Operation not permitted.'); } else { - send({data: [ item ]}); + send({data: [item]}); } }).then(() => { done({state: 'complete'}); diff --git a/server/src/endpoint/update.js b/server/src/endpoint/update.js index bfe1d3e86..2a37016b5 100644 --- a/server/src/endpoint/update.js +++ b/server/src/endpoint/update.js @@ -22,7 +22,7 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { collection.table.get(new_row('id')).do((old_row) => r.branch(old_row.eq(null), null, - [ old_row, old_row.merge(new_row) ]))) + [old_row, old_row.merge(new_row)]))) .run(conn, reql_options), (row, info) => writes.validate_old_row_required(row, info[0], info[1], ruleset), (rows) => // write to database, all valid rows diff --git a/server/src/endpoint/upsert.js b/server/src/endpoint/upsert.js index 69bea1065..91df393a1 100644 --- a/server/src/endpoint/upsert.js +++ b/server/src/endpoint/upsert.js @@ -22,9 +22,9 @@ const run = (raw_request, context, ruleset, metadata, send, done) => { r.branch(new_row.hasFields('id'), collection.table.get(new_row('id')).do((old_row) => r.branch(old_row.eq(null), - [ null, new_row ], - [ old_row, old_row.merge(new_row) ])), - [ null, new_row ])) + [null, new_row], + [old_row, old_row.merge(new_row)])), + [null, new_row])) .run(conn, reql_options), (row, info) => writes.validate_old_row_optional(row, info[0], info[1], ruleset), (rows) => // write to database, all valid rows diff --git a/server/src/endpoint/writes.js b/server/src/endpoint/writes.js index 56b10087e..ba1c7902d 100644 --- a/server/src/endpoint/writes.js +++ b/server/src/endpoint/writes.js @@ -81,7 +81,7 @@ const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, check(infos.length === row_data.length); // For each row to write (and info), validate it with permissions - const valid_rows = [ ]; + const valid_rows = []; row_data.forEach((data, i) => { const res = validate_row(data.row, infos[i]); @@ -96,14 +96,14 @@ const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, } }).then(() => { // For the set of valid rows, call the write step if (row_data.length === 0) { - return [ ]; + return []; } return do_write(row_data.map((data) => data.row)).then((res) => res.changes); }).then((changes) => { check(changes.length === row_data.length); // Remove successful writes and invalidated writes that had an initial version - const retry_rows = [ ]; + const retry_rows = []; row_data.forEach((data, index) => { const res = changes[index]; if (res.error !== undefined) { diff --git a/server/src/metadata/collection.js b/server/src/metadata/collection.js index b9417edfa..faca1335c 100644 --- a/server/src/metadata/collection.js +++ b/server/src/metadata/collection.js @@ -11,17 +11,17 @@ class Collection { this.table = r.db(db).table(name); // This is the ReQL Table object this._tables = new Map(); // A Map of Horizon Table objects this._registered = false; // Whether the `hz_collections` table says this collection exists - this._waiters = [ ]; + this._waiters = []; } close() { this._tables.forEach((table) => { table._waiters.forEach((w) => w(new Error('collection deleted'))); - table._waiters = [ ]; + table._waiters = []; table.close(); }); this._waiters.forEach((w) => w(new Error('collection deleted'))); - this._waiters = [ ]; + this._waiters = []; } _update_table(table_id, indexes, conn) { @@ -33,12 +33,12 @@ class Collection { } table.update_indexes(indexes, conn); this._waiters.forEach((w) => table.on_ready(w)); - this._waiters = [ ]; + this._waiters = []; } else { this._tables.delete(table_id); if (table) { table._waiters.forEach((w) => this.on_ready(w)); - table._waiters = [ ]; + table._waiters = []; table.close(); } } diff --git a/server/src/metadata/index.js b/server/src/metadata/index.js index 4e81788d6..a8744b127 100644 --- a/server/src/metadata/index.js +++ b/server/src/metadata/index.js @@ -16,7 +16,7 @@ const primary_index_name = 'id'; const name_to_info = (name) => { if (name === primary_index_name) { - return {geo: false, multi: false, fields: [ 'id' ]}; + return {geo: false, multi: false, fields: ['id']}; } const re = /^hz_(?:(geo)_)?(?:multi_([0-9])+_)?\[/; @@ -110,7 +110,7 @@ class Index { this.multi = info.multi; // false or the offset of the multi field this.fields = info.fields; // array of fields or nested field paths - this._waiters = [ ]; + this._waiters = []; this._result = null; if (this.geo) { @@ -124,11 +124,11 @@ class Index { logger.debug(`${table} index ready: ${name}`); this._result = true; this._waiters.forEach((w) => w()); - this._waiters = [ ]; + this._waiters = []; }).catch((err) => { this._result = err; this._waiters.forEach((w) => w(err)); - this._waiters = [ ]; + this._waiters = []; }); } else { logger.debug(`${table} index ready: ${name}`); @@ -138,7 +138,7 @@ class Index { close() { this._waiters.forEach((w) => w(new Error('index deleted'))); - this._waiters = [ ]; + this._waiters = []; } ready() { diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index 7b5f12c7b..2a85dfa55 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -9,7 +9,7 @@ const utils = require('../utils'); const r = require('rethinkdb'); const version_field = '$hz_v$'; -const metadata_version = [ 2, 0, 0 ]; +const metadata_version = [2, 0, 0]; const create_collection = (db, name, conn) => r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => @@ -25,7 +25,7 @@ const create_collection = (db, name, conn) => const initialize_metadata = (db, conn) => r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) .then(() => - Promise.all([ 'hz_collections', 'hz_users_auth', 'hz_groups' ].map((table) => + Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => r.branch(r.db(db).tableList().contains(table), { }, r.db(db).tableCreate(table)) @@ -103,7 +103,7 @@ class ReliableInit extends Reliable { }).then(() => { this.check_attempt(attempt); logger.debug('waiting for internal tables'); - return r.expr([ 'hz_collections', 'hz_users_auth', 'hz_groups', 'users' ]) + return r.expr(['hz_collections', 'hz_users_auth', 'hz_groups', 'users']) .forEach((table) => r.db(this._db).table(table).wait({timeout: 30})).run(conn); }).then(() => { this.check_attempt(attempt); @@ -114,7 +114,7 @@ class ReliableInit extends Reliable { r.branch(old_row.eq(null), { id: 'admin', - groups: [ 'admin' ], + groups: ['admin'], [version_field]: 0, }, old_row), diff --git a/server/src/metadata/table.js b/server/src/metadata/table.js index 3945ffc40..e218cb658 100644 --- a/server/src/metadata/table.js +++ b/server/src/metadata/table.js @@ -11,7 +11,7 @@ class Table { this.table = reql_table; this.indexes = new Map(); - this._waiters = [ ]; + this._waiters = []; this._result = null; this.table @@ -20,17 +20,17 @@ class Table { .then(() => { this._result = true; this._waiters.forEach((w) => w()); - this._waiters = [ ]; + this._waiters = []; }).catch((err) => { this._result = err; this._waiters.forEach((w) => w(err)); - this._waiters = [ ]; + this._waiters = []; }); } close() { this._waiters.forEach((w) => w(new Error('collection deleted'))); - this._waiters = [ ]; + this._waiters = []; this.indexes.forEach((i) => i.close()); this.indexes.clear(); @@ -64,7 +64,7 @@ class Table { if (old_index) { // Steal any waiters from the old index new_index._waiters = old_index._waiters; - old_index._waiters = [ ]; + old_index._waiters = []; } new_index_map.set(name, new_index); } catch (err) { diff --git a/server/src/permissions/rule.js b/server/src/permissions/rule.js index 6d7cd29a3..f00c40ccd 100644 --- a/server/src/permissions/rule.js +++ b/server/src/permissions/rule.js @@ -29,7 +29,7 @@ class Ruleset { } clear() { - this._rules = [ ]; + this._rules = []; } empty() { diff --git a/server/src/permissions/template.js b/server/src/permissions/template.js index 0bb73e6db..590007d4e 100644 --- a/server/src/permissions/template.js +++ b/server/src/permissions/template.js @@ -9,7 +9,7 @@ const vm = require('vm'); class Any { constructor(values) { - this._values = values || [ ]; + this._values = values || []; } matches(value, context) { @@ -55,7 +55,7 @@ class AnyObject { // specified at construction. class AnyArray { constructor(values) { - this._values = values || [ ]; + this._values = values || []; } matches(value, context) { @@ -87,7 +87,7 @@ const wrap_write = (query, docs) => { Array.isArray(docs)) { query.data = docs; } else { - query.data = [ docs ]; + query.data = [docs]; } return query; }; @@ -101,7 +101,7 @@ const wrap_remove = (doc) => { // Add helper methods to match any subset of the current query for reads or writes ast.TermBase.prototype.anyRead = function() { - return this._sendRequest(new Any([ 'query', 'subscribe' ]), + return this._sendRequest(new Any(['query', 'subscribe']), new AnyObject(this._query)); }; @@ -110,7 +110,7 @@ ast.Collection.prototype.anyWrite = function() { if (arguments.length === 0) { docs = new AnyArray(new Any()); } - return this._sendRequest(new Any([ 'store', 'upsert', 'insert', 'replace', 'update', 'remove' ]), + return this._sendRequest(new Any(['store', 'upsert', 'insert', 'replace', 'update', 'remove']), wrap_write(new AnyObject(this._query), docs)); }; diff --git a/server/src/reliable.js b/server/src/reliable.js index 330377eeb..21714db61 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -33,7 +33,7 @@ class Reliable { try { cbs.onReady.apply(cbs, this.ready); } catch (e) { - logger.error(`Unexpected error in reliable callback, `+ + logger.error('Unexpected error in reliable callback, ' + `event: subscribe onReady, error: ${e.stack}`); } } @@ -62,7 +62,7 @@ class Reliable { event.apply(sub.cbs, args); } } catch (e) { - logger.error(`Unexpected error in reliable callback, `+ + logger.error('Unexpected error in reliable callback, ' + `event: ${eventType}, error: ${e.stack}`); } }); @@ -114,7 +114,7 @@ class ReliableConn extends Reliable { close(reason) { let retProm = super.close(reason); if (this.conn) { - retProm = Promise.all([ retProm, this.conn.close() ]); + retProm = Promise.all([retProm, this.conn.close()]); } return retProm; } @@ -170,7 +170,7 @@ class ReliableChangefeed extends Reliable { close(reason) { let retProm = super.close(reason); if (this.cursor) { - retProm = Promise.all([ retProm, this.cursor.close() ]); + retProm = Promise.all([retProm, this.cursor.close()]); } if (this.subscription) { this.subscription.close(); diff --git a/server/src/response.js b/server/src/response.js index 46de0c3f2..08edec2f9 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -56,7 +56,7 @@ class Request { if (this._client._permissions_enabled) { const metadata = this._client._metadata; const user_info = this._client.user_info; - const matching_rules = [ ]; + const matching_rules = []; for (const group_name of user_info.groups) { const group = metadata.get_group(group_name); if (group !== undefined) { @@ -69,7 +69,7 @@ class Request { } this._ruleset.update(matching_rules); } else { - this._ruleset.update([ rule.any_rule ]); + this._ruleset.update([rule.any_rule]); } } diff --git a/server/src/server.js b/server/src/server.js index f65c80709..2a550e0c1 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -29,7 +29,7 @@ class Server extends EventEmitter { this._original_user_opts = user_opts; this._auth_methods = { }; this._request_handlers = new Map(); - this._ws_servers = [ ]; + this._ws_servers = []; this._close_promise = null; this._default_middleware = (req, res, next) => { next(new Error('No middleware to handle the request.')); diff --git a/server/src/test/permissions.js b/server/src/test/permissions.js index 3fc76e5f3..1ff1ba178 100644 --- a/server/src/test/permissions.js +++ b/server/src/test/permissions.js @@ -12,18 +12,18 @@ const make_request = (type, collection, options) => { } }; -const context = {id: 123, groups: [ 'admin', 'default', 'authenticated' ]}; +const context = {id: 123, groups: ['admin', 'default', 'authenticated']}; describe('Permissions', () => { describe('Template', () => { it('any', () => { const rule = new Rule('foo', {template: 'any()'}); - const tests = [ { }, + const tests = [{ }, {type: 'query', options: {collection: 'test'}}, {fake: 'bar'}, {options: { }}, - {type: 'query', options: {fake: 'baz'}} ]; + {type: 'query', options: {fake: 'baz'}}]; for (const t of tests) { assert(rule.is_match(t, context)); @@ -40,12 +40,12 @@ describe('Permissions', () => { assert(rule.is_match(make_request('query', 'fake', { }), context)); assert(rule.is_match(make_request('query', 'fake', {find: { }}), context)); assert(rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [ { }, { } ]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }, { }]}), context)); assert(!rule.is_match(make_request('subscribe', null, { }), context)); assert(rule.is_match(make_request('subscribe', 'fake', { }), context)); assert(rule.is_match(make_request('subscribe', 'fake', {find: { }}), context)); assert(rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(rule.is_match(make_request('subscribe', 'test', {find_all: [ { }, { } ]}), context)); + assert(rule.is_match(make_request('subscribe', 'test', {find_all: [{ }, { }]}), context)); }); it('any read with collection', () => { @@ -65,17 +65,17 @@ describe('Permissions', () => { // TODO: allow for any number of fields in order const rule = new Rule('foo', {template: 'collection("test").order(any(), any()).anyRead()'}); assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {order: [ 'foo', 'ascending' ]}), context)); + assert(!rule.is_match(make_request('query', 'fake', {order: ['foo', 'ascending']}), context)); assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(!rule.is_match(make_request('query', 'test', {order: [ 'baz' ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {order: [ 'baz', 'fake' ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {order: [ [ 'fake' ] ]}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [ [ 'foo' ], 'ascending' ]}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [ [ 'bar' ], 'descending' ]}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [ [ 'baz' ], 'fake' ]}), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }, order: [ [ 'baz' ], 'fake' ]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [ { } ], order: [ [ 'baz' ], 'fake' ]}), context)); - assert(rule.is_match(make_request('query', 'test', {fake: 'baz', order: [ [ 'baz' ], 'fake' ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: ['baz']}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: ['baz', 'fake']}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: [['fake']]}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [['foo'], 'ascending']}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [['bar'], 'descending']}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [['baz'], 'fake']}), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); + assert(rule.is_match(make_request('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); }); it('any read with find', () => { @@ -93,8 +93,8 @@ describe('Permissions', () => { assert(rule.is_valid()); assert(!rule.is_match(make_request('query', 'fake', {find_all: { }}), context)); assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [ { } ]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [ { } ], fake: 'baz'}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); }); it('single key in findAll', () => { @@ -102,11 +102,11 @@ describe('Permissions', () => { assert(rule.is_valid()); assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ {bar: 'baz'} ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: (context.id + 1)} ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id, bar: 'baz'} ]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id} ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id}]}), context)); }); it('multiple keys in findAll', () => { @@ -114,11 +114,11 @@ describe('Permissions', () => { assert(rule.is_valid()); assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ {bar: 'baz'} ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: (context.id + 1)} ]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id, bar: 'baz'} ]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [ {owner: context.id, key: 123} ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, key: 123}]}), context)); }); it('collection fetch', () => { @@ -139,27 +139,27 @@ describe('Permissions', () => { assert(rule.is_match(make_request('subscribe', 'test', { }), context)); }); - for (const type of [ 'store', 'update', 'insert', 'upsert', 'replace', 'remove' ]) { + for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { it(`collection ${type}`, () => { const rule = new Rule('foo', {template: `collection("test").${type}(any())`}); assert(rule.is_valid()); assert(!rule.is_match(make_request(type, 'test', { }), context)); assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [ ]}), context)); - assert(!rule.is_match(make_request(type, 'fake', {data: [ { } ]}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [ { } ], fake: 6}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ { } ]}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: []}), context)); + assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); }); it(`collection ${type} batch`, () => { const rule = new Rule('foo', {template: `collection("test").${type}(anyArray(any()))`}); assert(rule.is_valid()); assert(!rule.is_match(make_request(type, 'test', { }), context)); assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [ { } ], fake: 6}), context)); - assert(!rule.is_match(make_request(type, 'fake', {data: [ { } ]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ ]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ { } ]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ { }, {bar: 'baz'} ]}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); + assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: []}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); }); } @@ -170,11 +170,11 @@ describe('Permissions', () => { assert(!rule.is_match(make_request('query', 'test', { }), context)); assert(!rule.is_match(make_request('store', null, { }), context)); - for (const type of [ 'store', 'update', 'insert', 'upsert', 'replace', 'remove' ]) { + for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { assert(!rule.is_match(make_request(type, 'fake', { }), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ ]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ { } ]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [ ], bar: 'baz'}), context)); + assert(rule.is_match(make_request(type, 'test', {data: []}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [], bar: 'baz'}), context)); } }); @@ -183,7 +183,7 @@ describe('Permissions', () => { assert(rule.is_valid()); assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); assert(!rule.is_match(make_request('query', 'test', {find: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: [ ]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: []}), context)); assert(!rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); assert(!rule.is_match(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); assert(!rule.is_match(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); diff --git a/server/src/test/prereq_tests.js b/server/src/test/prereq_tests.js index bc8c6b7b3..81553f6db 100644 --- a/server/src/test/prereq_tests.js +++ b/server/src/test/prereq_tests.js @@ -49,7 +49,7 @@ const all_tests = (collection) => { type: 'insert', options: { collection: rand_collection, - data: [ { } ], + data: [{ }], }, }, (err, res) => { @@ -80,7 +80,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - order: [ [ field_name ], 'ascending' ], + order: [[field_name], 'ascending'], }, }, (err, res) => { diff --git a/server/src/test/protocol_tests.js b/server/src/test/protocol_tests.js index 4f6fa71f3..09eaec786 100644 --- a/server/src/test/protocol_tests.js +++ b/server/src/test/protocol_tests.js @@ -31,7 +31,7 @@ const all_tests = (collection) => { it('no type', (done) => { utils.stream_test({request_id: 0}, (err, res) => { - assert.deepStrictEqual(res, [ ]); + assert.deepStrictEqual(res, []); utils.check_error(err, '"type" is required'); done(); }); @@ -39,7 +39,7 @@ const all_tests = (collection) => { it('no options', (done) => { utils.stream_test({request_id: 1, type: 'fake'}, (err, res) => { - assert.deepStrictEqual(res, [ ]); + assert.deepStrictEqual(res, []); utils.check_error(err, '"options" is required'); done(); }); @@ -47,7 +47,7 @@ const all_tests = (collection) => { it('invalid endpoint', (done) => { utils.stream_test({request_id: 2, type: 'fake', options: { }}, (err, res) => { - assert.deepStrictEqual(res, [ ]); + assert.deepStrictEqual(res, []); assert.strictEqual(err.message, '"fake" is not a registered request type.'); done(); }); diff --git a/server/src/test/query_tests.js b/server/src/test/query_tests.js index 63e1a5f7f..6a091fb46 100644 --- a/server/src/test/query_tests.js +++ b/server/src/test/query_tests.js @@ -33,7 +33,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - order: [ [ 'id' ], 'ascending' ], + order: [['id'], 'ascending'], }, }, (err, res) => { @@ -67,7 +67,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - order: [ [ 'id' ], 'descending' ], + order: [['id'], 'descending'], limit: 4, }, }, @@ -85,7 +85,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - above: [ {id: 5}, 'closed' ], + above: [{id: 5}, 'closed'], }, }, (err, res) => { @@ -102,7 +102,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - below: [ {id: 5}, 'closed' ], + below: [{id: 5}, 'closed'], }, }, (err, res) => { @@ -119,8 +119,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - above: [ {id: 5}, 'open' ], - below: [ {id: 7}, 'open' ], + above: [{id: 5}, 'open'], + below: [{id: 7}, 'open'], }, }, (err, res) => { @@ -159,7 +159,7 @@ const all_tests = (collection) => { }, (err, res) => { assert.ifError(err); - assert.deepStrictEqual(res, [ ]); + assert.deepStrictEqual(res, []); done(); }); }); @@ -171,7 +171,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {id: 4}, {id: 6}, {id: 9} ], + find_all: [{id: 4}, {id: 6}, {id: 9}], }, }, (err, res) => { @@ -188,8 +188,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {id: 1} ], - order: [ [ 'value' ], 'descending' ], + find_all: [{id: 1}], + order: [['value'], 'descending'], }, }, (err, res) => { @@ -206,7 +206,7 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {id: 4}, {id: 8}, {id: 2}, {id: 1} ], + find_all: [{id: 4}, {id: 8}, {id: 2}, {id: 1}], limit: 3, }, }, @@ -224,8 +224,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {id: 4} ], - order: [ [ 'value' ], 'descending' ], + find_all: [{id: 4}], + order: [['value'], 'descending'], limit: 3, }, }, @@ -243,8 +243,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 1} ], - above: [ {id: 3}, 'open' ], + find_all: [{value: 1}], + above: [{id: 3}, 'open'], }, }, (err, res) => { @@ -261,8 +261,8 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 1} ], - below: [ {id: 5}, 'open' ], + find_all: [{value: 1}], + below: [{id: 5}, 'open'], }, }, (err, res) => { @@ -279,9 +279,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 1} ], - above: [ {id: 1}, 'closed' ], - below: [ {id: 9}, 'open' ], + find_all: [{value: 1}], + above: [{id: 1}, 'closed'], + below: [{id: 9}, 'open'], }, }, (err, res) => { @@ -298,9 +298,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 1} ], - order: [ [ 'id' ], 'ascending' ], - above: [ {id: 7}, 'open' ], + find_all: [{value: 1}], + order: [['id'], 'ascending'], + above: [{id: 7}, 'open'], }, }, (err, res) => { @@ -317,9 +317,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 0} ], - order: [ [ 'id' ], 'descending' ], - below: [ {id: 8}, 'open' ], + find_all: [{value: 0}], + order: [['id'], 'descending'], + below: [{id: 8}, 'open'], }, }, (err, res) => { @@ -336,10 +336,10 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 0} ], - order: [ [ 'id' ], 'descending' ], - above: [ {id: 3}, 'closed' ], - below: [ {id: 9}, 'closed' ], + find_all: [{value: 0}], + order: [['id'], 'descending'], + above: [{id: 3}, 'closed'], + below: [{id: 9}, 'closed'], }, }, (err, res) => { @@ -358,9 +358,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 0} ], - order: [ [ 'value', 'a' ], 'descending' ], - above: [ {b: 4}, 'closed' ], + find_all: [{value: 0}], + order: [['value', 'a'], 'descending'], + above: [{b: 4}, 'closed'], }, }, (err) => { @@ -376,9 +376,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 0} ], - order: [ [ 'value', 'a' ], 'descending' ], - above: [ {a: 4}, 'closed' ], + find_all: [{value: 0}], + order: [['value', 'a'], 'descending'], + above: [{a: 4}, 'closed'], }, }, (err) => { @@ -394,9 +394,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 0} ], - order: [ [ 'value', 'a' ], 'descending' ], - below: [ {b: 4}, 'closed' ], + find_all: [{value: 0}], + order: [['value', 'a'], 'descending'], + below: [{b: 4}, 'closed'], }, }, (err) => { @@ -412,9 +412,9 @@ const all_tests = (collection) => { type: 'query', options: { collection, - find_all: [ {value: 0} ], - order: [ [ 'value', 'a' ], 'descending' ], - below: [ {a: 4}, 'closed' ], + find_all: [{value: 0}], + order: [['value', 'a'], 'descending'], + below: [{a: 4}, 'closed'], }, }, (err) => { diff --git a/server/src/test/schema.js b/server/src/test/schema.js index c3ee6e43f..1991f5849 100644 --- a/server/src/test/schema.js +++ b/server/src/test/schema.js @@ -38,7 +38,7 @@ describe('Schema', () => { it('required fields', () => { test_required_fields(horizon_protocol.request, valid, - [ 'request_id', 'type', 'options' ]); + ['request_id', 'type', 'options']); }); it('wrong "request_id" type', () => { @@ -57,7 +57,7 @@ describe('Schema', () => { it('wrong "options" type', () => { const request = Object.assign({}, valid); - request.options = [ 5, 6 ]; + request.options = [5, 6]; const error = horizon_protocol.request.validate(request).error; utils.check_error(error, '"options" must be an object'); }); @@ -70,12 +70,12 @@ describe('Schema', () => { describe('Write', () => { const write_without_id = { collection: 'horizon', - data: [ {field: 4} ], + data: [{field: 4}], }; const write_with_id = { collection: 'horizon', - data: [ {id: 5, field: 4} ], + data: [{id: 5, field: 4}], }; // In order to reduce the number of tests, these were written assuming @@ -105,7 +105,7 @@ describe('Schema', () => { it('required fields', () => { test_required_fields(horizon_protocol.insert, write_with_id, - [ 'collection', 'data' ]); + ['collection', 'data']); }); it('extra field', () => { @@ -135,14 +135,14 @@ describe('Schema', () => { it('wrong "data" member type', () => { const request = Object.assign({}, write_with_id); - request.data = [ 7 ]; + request.data = [7]; const error = horizon_protocol.insert.validate(request).error; utils.check_error(error, '"0" must be an object'); }); it('empty "data" array', () => { const request = Object.assign({}, write_with_id); - request.data = [ ]; + request.data = []; const error = horizon_protocol.insert.validate(request).error; utils.check_error(error, '"data" must contain at least 1 items'); }); @@ -161,7 +161,7 @@ describe('Schema', () => { it('required fields', () => { test_required_fields(horizon_protocol.replace, write_with_id, - [ 'collection', 'data' ]); + ['collection', 'data']); }); it('extra field', () => { @@ -191,14 +191,14 @@ describe('Schema', () => { it('wrong "data" member type', () => { const request = Object.assign({}, write_with_id); - request.data = [ 7 ]; + request.data = [7]; const error = horizon_protocol.replace.validate(request).error; utils.check_error(error, '"0" must be an object'); }); it('empty "data" array', () => { const request = Object.assign({}, write_with_id); - request.data = [ ]; + request.data = []; const error = horizon_protocol.replace.validate(request).error; utils.check_error(error, '"data" must contain at least 1 items'); }); @@ -224,7 +224,7 @@ describe('Schema', () => { it('required fields', () => { test_required_fields(horizon_protocol.query, valid, - [ 'collection' ]); + ['collection']); }); it('extra field', () => { @@ -233,7 +233,7 @@ describe('Schema', () => { it('order', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; + request.order = [['id'], 'ascending']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -241,8 +241,8 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; - request.above = [ {id: 10}, 'open' ]; + request.order = [['id'], 'ascending']; + request.above = [{id: 10}, 'open']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -250,8 +250,8 @@ describe('Schema', () => { it('below', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; - request.below = [ {id: 5}, 'open' ]; + request.order = [['id'], 'ascending']; + request.below = [{id: 5}, 'open']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -267,9 +267,9 @@ describe('Schema', () => { it('above and below and limit', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; - request.below = [ {id: 0}, 'closed' ]; - request.below = [ {id: 5}, 'closed' ]; + request.order = [['id'], 'ascending']; + request.below = [{id: 0}, 'closed']; + request.below = [{id: 5}, 'closed']; request.limit = 4; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); @@ -300,23 +300,23 @@ describe('Schema', () => { it('wrong "order" value', () => { const request = Object.assign({}, valid); { - request.order = [ ]; + request.order = []; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"order" does not contain [fields, direction]'); } { - request.order = [ [ 'id' ] ]; + request.order = [['id']]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"order" does not contain [direction]'); } { - request.order = [ { }, 'ascending' ]; + request.order = [{ }, 'ascending']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"fields" must be an array'); } { - request.order = [ [ ], 'descending' ]; + request.order = [[], 'descending']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"fields" must contain at least 1 item'); } { - request.order = [ [ 'field' ], 'baleeted' ]; + request.order = [['field'], 'baleeted']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"direction" must be one of [ascending, descending]'); } @@ -324,7 +324,7 @@ describe('Schema', () => { it('"above" without "order"', () => { const request = Object.assign({}, valid); - request.above = [ {id: 5}, 'open' ]; + request.above = [{id: 5}, 'open']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -340,23 +340,23 @@ describe('Schema', () => { it('wrong "above" value', () => { const request = Object.assign({}, valid); { - request.above = [ ]; + request.above = []; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"above" does not contain [value, bound_type]'); } { - request.above = [ 1, 'closed' ]; + request.above = [1, 'closed']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"value" must be an object'); } { - request.above = [ { }, 'open' ]; + request.above = [{ }, 'open']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"value" must have 1 child'); } { - request.above = [ {id: 4}, 5 ]; + request.above = [{id: 4}, 5]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be a string'); } { - request.above = [ {id: 3}, 'ajar' ]; + request.above = [{id: 3}, 'ajar']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be one of [open, closed]'); } @@ -364,7 +364,7 @@ describe('Schema', () => { it('"below" without "order"', () => { const request = Object.assign({}, valid); - request.below = [ {id: 1}, 'open' ]; + request.below = [{id: 1}, 'open']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -372,7 +372,7 @@ describe('Schema', () => { it('wrong "below" type', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; + request.order = [['id'], 'ascending']; request.below = true; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"below" must be an array'); @@ -380,25 +380,25 @@ describe('Schema', () => { it('wrong "below" value', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; + request.order = [['id'], 'ascending']; { - request.below = [ ]; + request.below = []; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"below" does not contain [value, bound_type]'); } { - request.below = [ 1, 'closed' ]; + request.below = [1, 'closed']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"value" must be an object'); } { - request.below = [ { }, 'open' ]; + request.below = [{ }, 'open']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"value" must have 1 child'); } { - request.below = [ {id: 4}, 5 ]; + request.below = [{id: 4}, 5]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be a string'); } { - request.below = [ {id: 3}, 'ajar' ]; + request.below = [{id: 3}, 'ajar']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"bound_type" must be one of [open, closed]'); } @@ -439,21 +439,21 @@ describe('Schema', () => { it('order', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; + request.order = [['id'], 'ascending']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"order" is not allowed'); }); it('above', () => { const request = Object.assign({}, valid); - request.above = [ {id: 3}, 'open' ]; + request.above = [{id: 3}, 'open']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"above" is not allowed'); }); it('below', () => { const request = Object.assign({}, valid); - request.below = [ {id: 4}, 'closed' ]; + request.below = [{id: 4}, 'closed']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"below" is not allowed'); }); @@ -476,7 +476,7 @@ describe('Schema', () => { describe('find_all multiple', () => { const valid = { collection: 'horizon', - find_all: [ {score: 2}, {score: 5, id: 0} ], + find_all: [{score: 2}, {score: 5, id: 0}], }; it('valid', () => { @@ -487,7 +487,7 @@ describe('Schema', () => { it('order', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'descending' ]; + request.order = [['id'], 'descending']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"order" is not allowed'); }); @@ -503,11 +503,11 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); { - request.above = [ {id: 3}, 'closed' ]; + request.above = [{id: 3}, 'closed']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"above" is not allowed'); } { - request.order = [ [ 'id' ], 'ascending' ]; + request.order = [['id'], 'ascending']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"order" is not allowed'); } @@ -516,11 +516,11 @@ describe('Schema', () => { it('below', () => { const request = Object.assign({}, valid); { - request.below = [ {id: 9}, 'closed' ]; + request.below = [{id: 9}, 'closed']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"below" is not allowed'); } { - request.order = [ [ 'id' ], 'descending' ]; + request.order = [['id'], 'descending']; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"order" is not allowed'); } @@ -530,7 +530,7 @@ describe('Schema', () => { describe('find_all one', () => { const valid = { collection: 'horizon', - find_all: [ {score: 8, id: 5} ], + find_all: [{score: 8, id: 5}], }; it('valid', () => { @@ -541,7 +541,7 @@ describe('Schema', () => { it('order', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'descending' ]; + request.order = [['id'], 'descending']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -557,8 +557,8 @@ describe('Schema', () => { it('above', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'ascending' ]; - request.above = [ {id: 3}, 'closed' ]; + request.order = [['id'], 'ascending']; + request.above = [{id: 3}, 'closed']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -566,8 +566,8 @@ describe('Schema', () => { it('below', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'descending' ]; - request.below = [ {id: 9}, 'closed' ]; + request.order = [['id'], 'descending']; + request.below = [{id: 9}, 'closed']; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); assert.deepStrictEqual(parsed.value, request); @@ -575,9 +575,9 @@ describe('Schema', () => { it('above and below and limit', () => { const request = Object.assign({}, valid); - request.order = [ [ 'id' ], 'descending' ]; - request.above = [ {id: 'foo'}, 'open' ]; - request.below = [ {id: 'bar'}, 'closed' ]; + request.order = [['id'], 'descending']; + request.above = [{id: 'foo'}, 'open']; + request.below = [{id: 'bar'}, 'closed']; request.limit = 59; const parsed = horizon_protocol.query.validate(request); assert.ifError(parsed.error); @@ -594,11 +594,11 @@ describe('Schema', () => { it('wrong "find_all" value', () => { const request = Object.assign({}, valid); { - request.find_all = [ ]; + request.find_all = []; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"find_all" must contain at least 1 items'); } { - request.find_all = [ { } ]; + request.find_all = [{ }]; const error = horizon_protocol.query.validate(request).error; utils.check_error(error, '"item" must have at least 1 child'); } diff --git a/server/src/test/test.js b/server/src/test/test.js index ddf1a740c..eb96a6406 100644 --- a/server/src/test/test.js +++ b/server/src/test/test.js @@ -5,11 +5,11 @@ require('source-map-support').install(); const logger = require('../logger'); const utils = require('./utils'); -const all_suites = [ 'prereq_tests', +const all_suites = ['prereq_tests', 'protocol_tests', 'query_tests', 'subscribe_tests', - 'write_tests' ]; + 'write_tests']; const collection = 'test'; before('Start RethinkDB Server', () => utils.start_rethinkdb()); diff --git a/server/src/test/write_tests.js b/server/src/test/write_tests.js index 4d40e84c4..a2d216c23 100644 --- a/server/src/test/write_tests.js +++ b/server/src/test/write_tests.js @@ -11,16 +11,16 @@ const invalidated_msg = horizon_writes.invalidated_msg; // Before each test, ids [0, 4) will be present in the collection const original_data = [ - {id: 0, old_field: [ ], [hz_v]: 0}, - {id: 1, old_field: [ ], [hz_v]: 0}, - {id: 2, old_field: [ ], [hz_v]: 0}, - {id: 3, old_field: [ ], [hz_v]: 0}, + {id: 0, old_field: [], [hz_v]: 0}, + {id: 1, old_field: [], [hz_v]: 0}, + {id: 2, old_field: [], [hz_v]: 0}, + {id: 3, old_field: [], [hz_v]: 0}, ]; -const new_id = [ 4 ]; -const conflict_id = [ 3 ]; -const new_ids = [ 4, 5, 6 ]; -const conflict_ids = [ 2, 3, 4 ]; +const new_id = [4]; +const conflict_id = [3]; +const new_ids = [4, 5, 6]; +const conflict_ids = [2, 3, 4]; const without_version = (item) => { const res = Object.assign({ }, item); @@ -43,7 +43,7 @@ const all_tests = (collection) => { const new_row_from_id = (id) => ({id, new_field: 'a'}); const merged_row_from_id = (id) => { if (id >= 4) { return new_row_from_id(id); } - return {id, new_field: 'a', old_field: [ ]}; + return {id, new_field: 'a', old_field: []}; }; const make_request = (type, data, options) => ({ @@ -212,25 +212,25 @@ const all_tests = (collection) => { describe('Versioned', () => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); - const test_data = [ {id: 'versioned', [hz_v]: 11, foo: 'bar'} ]; + const test_data = [{id: 'versioned', [hz_v]: 11, foo: 'bar'}]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); describe('Store', () => { - const request = (row) => make_request('store', [ row ]); + const request = (row) => make_request('store', [row]); it('correct version', (done) => { utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versioned', [hz_v]: 12} ]; + const expected = [{id: 'versioned', [hz_v]: 12}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versioned', [hz_v]: 12, value: 1} ], done); + check_collection([{id: 'versioned', [hz_v]: 12, value: 1}], done); }); }); it('incorrect version', (done) => { utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -238,21 +238,21 @@ const all_tests = (collection) => { }); describe('Replace', () => { - const request = (row) => make_request('replace', [ row ]); + const request = (row) => make_request('replace', [row]); it('correct version', (done) => { utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versioned', [hz_v]: 12} ]; + const expected = [{id: 'versioned', [hz_v]: 12}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versioned', [hz_v]: 12, value: 1} ], done); + check_collection([{id: 'versioned', [hz_v]: 12, value: 1}], done); }); }); it('incorrect version', (done) => { utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -260,21 +260,21 @@ const all_tests = (collection) => { }); describe('Upsert', () => { - const request = (row) => make_request('upsert', [ row ]); + const request = (row) => make_request('upsert', [row]); it('correct version', (done) => { utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versioned', [hz_v]: 12} ]; + const expected = [{id: 'versioned', [hz_v]: 12}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar'} ], done); + check_collection([{id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar'}], done); }); }); it('incorrect version', (done) => { utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -282,21 +282,21 @@ const all_tests = (collection) => { }); describe('Update', () => { - const request = (row) => make_request('update', [ row ]); + const request = (row) => make_request('update', [row]); it('correct version', (done) => { utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versioned', [hz_v]: 12} ]; + const expected = [{id: 'versioned', [hz_v]: 12}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar'} ], done); + check_collection([{id: 'versioned', [hz_v]: 12, value: 1, foo: 'bar'}], done); }); }); it('incorrect version', (done) => { utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -304,21 +304,21 @@ const all_tests = (collection) => { }); describe('Remove', () => { - const request = (row) => make_request('remove', [ row ]); + const request = (row) => make_request('remove', [row]); it('correct version', (done) => { utils.stream_test(request({id: 'versioned', value: 1, [hz_v]: 11}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versioned', [hz_v]: 11} ]; + const expected = [{id: 'versioned', [hz_v]: 11}]; assert.deepStrictEqual(res, expected); - check_collection([ ], done); + check_collection([], done); }); }); it('incorrect version', (done) => { utils.stream_test(request({id: 'versioned', value: 2, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -329,25 +329,25 @@ const all_tests = (collection) => { describe('Versionless', () => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); - const test_data = [ {id: 'versionless', foo: 'bar'} ]; + const test_data = [{id: 'versionless', foo: 'bar'}]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); describe('Store', () => { - const request = (row) => make_request('store', [ row ]); + const request = (row) => make_request('store', [row]); it('unspecified version', (done) => { utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versionless', [hz_v]: 0} ]; + const expected = [{id: 'versionless', [hz_v]: 0}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versionless', [hz_v]: 0, value: 3} ], done); + check_collection([{id: 'versionless', [hz_v]: 0, value: 3}], done); }); }); it('specified version', (done) => { utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -355,21 +355,21 @@ const all_tests = (collection) => { }); describe('Replace', () => { - const request = (row) => make_request('replace', [ row ]); + const request = (row) => make_request('replace', [row]); it('unspecified version', (done) => { utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versionless', [hz_v]: 0} ]; + const expected = [{id: 'versionless', [hz_v]: 0}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versionless', [hz_v]: 0, value: 3} ], done); + check_collection([{id: 'versionless', [hz_v]: 0, value: 3}], done); }); }); it('specified version', (done) => { utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -377,21 +377,21 @@ const all_tests = (collection) => { }); describe('Upsert', () => { - const request = (row) => make_request('upsert', [ row ]); + const request = (row) => make_request('upsert', [row]); it('unspecified version', (done) => { utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versionless', [hz_v]: 0} ]; + const expected = [{id: 'versionless', [hz_v]: 0}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar'} ], done); + check_collection([{id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar'}], done); }); }); it('specified version', (done) => { utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -399,21 +399,21 @@ const all_tests = (collection) => { }); describe('Update', () => { - const request = (row) => make_request('update', [ row ]); + const request = (row) => make_request('update', [row]); it('unspecified version', (done) => { utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versionless', [hz_v]: 0} ]; + const expected = [{id: 'versionless', [hz_v]: 0}]; assert.deepStrictEqual(res, expected); - check_collection([ {id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar'} ], done); + check_collection([{id: 'versionless', [hz_v]: 0, value: 3, foo: 'bar'}], done); }); }); it('specified version', (done) => { utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -421,21 +421,21 @@ const all_tests = (collection) => { }); describe('Remove', () => { - const request = (row) => make_request('remove', [ row ]); + const request = (row) => make_request('remove', [row]); it('unspecified version', (done) => { utils.stream_test(request({id: 'versionless', value: 3}), (err, res) => { assert.ifError(err); - const expected = [ {id: 'versionless'} ]; + const expected = [{id: 'versionless'}]; assert.deepStrictEqual(res, expected); - check_collection([ ], done); + check_collection([], done); }); }); it('specified version', (done) => { utils.stream_test(request({id: 'versionless', value: 4, [hz_v]: 5}), (err, res) => { assert.ifError(err); - const expected = [ {error: invalidated_msg} ]; + const expected = [{error: invalidated_msg}]; assert.deepStrictEqual(res, expected); check_collection(test_data, done); }); @@ -472,9 +472,9 @@ const all_tests = (collection) => { const latest_index = res.findIndex((x) => x[hz_v] === 2); assert(latest_index !== -1); res.sort(by_version); - assert.deepStrictEqual(res, [ {id: 0, [hz_v]: 0}, + assert.deepStrictEqual(res, [{id: 0, [hz_v]: 0}, {id: 0, [hz_v]: 1}, - {id: 0, [hz_v]: 2} ]); + {id: 0, [hz_v]: 2}]); return writes[latest_index]; }; @@ -495,14 +495,14 @@ const all_tests = (collection) => { }; describe('Existing Row', () => { - const test_data = [ {id: 0, value: 0} ]; + const test_data = [{id: 0, value: 0}]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); it('Store', (done) => { utils.stream_test(make_request('store', writes), (err, res) => { assert.ifError(err); const latest_write = check_and_get_latest_write(res); - check_collection([ Object.assign({[hz_v]: 2}, latest_write) ], done); + check_collection([Object.assign({[hz_v]: 2}, latest_write)], done); }); }); @@ -510,7 +510,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('replace', writes), (err, res) => { assert.ifError(err); const latest_write = check_and_get_latest_write(res); - check_collection([ Object.assign({[hz_v]: 2}, latest_write) ], done); + check_collection([Object.assign({[hz_v]: 2}, latest_write)], done); }); }); @@ -518,7 +518,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('upsert', writes), (err, res) => { assert.ifError(err); check_and_get_latest_write(res); - check_collection([ {id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2} ], done); + check_collection([{id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2}], done); }); }); @@ -526,16 +526,16 @@ const all_tests = (collection) => { utils.stream_test(make_request('update', writes), (err, res) => { assert.ifError(err); check_and_get_latest_write(res); - check_collection([ {id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2} ], done); + check_collection([{id: 0, value: 0, a: 1, b: 2, c: 3, [hz_v]: 2}], done); }); }); it('Remove', (done) => { utils.stream_test(make_request('remove', writes), (err, res) => { assert.ifError(err); - assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [ undefined, undefined, undefined ]); - assert.deepStrictEqual(res.map((x) => x.id), [ 0, 0, 0 ]); - check_collection([ ], done); + assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [undefined, undefined, undefined]); + assert.deepStrictEqual(res.map((x) => x.id), [0, 0, 0]); + check_collection([], done); }); }); }); @@ -545,7 +545,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('insert', writes), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'The document already exists.'); - check_collection([ Object.assign({[hz_v]: 0}, success_write) ], done); + check_collection([Object.assign({[hz_v]: 0}, success_write)], done); }); }); @@ -553,16 +553,16 @@ const all_tests = (collection) => { utils.stream_test(make_request('store', writes), (err, res) => { assert.ifError(err); const latest_write = check_and_get_latest_write(res); - check_collection([ Object.assign({[hz_v]: 2}, latest_write) ], done); + check_collection([Object.assign({[hz_v]: 2}, latest_write)], done); }); }); it('Upsert', (done) => { utils.stream_test(make_request('upsert', writes), (err, res) => { assert.ifError(err); - assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [ 0, 1, 2 ]); - assert.deepStrictEqual(res.map((x) => x.id), [ 0, 0, 0 ]); - check_collection([ {id: 0, a: 1, b: 2, c: 3, [hz_v]: 2} ], done); + assert.deepStrictEqual(res.map((x) => x[hz_v]).sort(), [0, 1, 2]); + assert.deepStrictEqual(res.map((x) => x.id), [0, 0, 0]); + check_collection([{id: 0, a: 1, b: 2, c: 3, [hz_v]: 2}], done); }); }); }); @@ -573,14 +573,14 @@ const all_tests = (collection) => { // timeout of zero, so the other rows should immediately error. describe('Zero Timeout', () => { const timeout = {timeout: 0}; - const test_data = [ {id: 0, value: 0} ]; + const test_data = [{id: 0, value: 0}]; beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); it('Store', (done) => { utils.stream_test(make_request('store', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({[hz_v]: 0}, success_write) ], done); + check_collection([Object.assign({[hz_v]: 0}, success_write)], done); }); }); @@ -588,7 +588,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('replace', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({[hz_v]: 0}, success_write) ], done); + check_collection([Object.assign({[hz_v]: 0}, success_write)], done); }); }); @@ -596,7 +596,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('upsert', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({[hz_v]: 0}, test_data[0], success_write) ], done); + check_collection([Object.assign({[hz_v]: 0}, test_data[0], success_write)], done); }); }); @@ -604,7 +604,7 @@ const all_tests = (collection) => { utils.stream_test(make_request('update', writes, timeout), (err, res) => { assert.ifError(err); const success_write = check_one_successful_write(res, 'Operation timed out.'); - check_collection([ Object.assign({[hz_v]: 0}, test_data[0], success_write) ], done); + check_collection([Object.assign({[hz_v]: 0}, test_data[0], success_write)], done); }); }); }); diff --git a/server/src/utils.js b/server/src/utils.js index d1c0f65e1..9927c5405 100644 --- a/server/src/utils.js +++ b/server/src/utils.js @@ -1,6 +1,6 @@ 'use strict'; -const MIN_VERSION = [ 2, 3, 1 ]; +const MIN_VERSION = [2, 3, 1]; // Recursive version compare, could be flatter but opted for instant return if // comparison is greater rather than continuing to compare to end. From 54ba00ae66fbbfc25ce806c347d39f3af069ddd6 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Wed, 24 Aug 2016 01:10:25 +0000 Subject: [PATCH 26/86] checkpoint --- plugins/permissions/src/permissions.js | 79 +++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 91e2e6b2e..762869c36 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -1,5 +1,9 @@ 'use strict'; +// We can be desynced from the database for up to 5 seconds before we +// start rejecting queries. +const staleLimit = 5000; + // auth plugins should set 'request.context.user' // token/anonymous: this should be the user id // unauthenticated: this should be null, and will use the default rules @@ -15,6 +19,7 @@ addToMapSet(map, name, el) { set.add(el); } +// Returns whether or not it deleted an empty set from the map. delFromMapSet(map, name, el) { let set = map.get(name); assert(set, `delFromMapSet: ${name} not in map`); @@ -22,13 +27,16 @@ delFromMapSet(map, name, el) { set.delete(el); if (set.size === 0) { map.delete(name); + return true; } + return false; } getMapSet(map, name) { return map.get(name) || new Set(); } +const emptyRulesetSymbol = Symbol(); class RuleMap { constructor() { this.groupToRulenames = new Map(); @@ -36,8 +44,19 @@ class RuleMap { this.groupToUsers = new Map(); this.userToRulenames = new Map(); // computed - // RSI: pick up here, update this in the functions below. - this.userToTimestamps = new Map(); // updated when user's rules change + this.userToRulesetSymbol = new Map(); // updated when user's rules change + } + + getUserRulesetSymbol(user) { + return this.userToRulesetSymbol.get(user) || emptyRulesetSymbol; + } + + forEachUserRule(user, cb) { + this.userToRulenames.forEach((rn) => { + const rule = this.RulenameToRule.get(rn); + assert(rule); + cb(rule); + }); } addUserGroup(user, group) { @@ -45,13 +64,21 @@ class RuleMap { getMapSet(this.groupToRulenames, group).forEach((rn) => { addToMapSet(this.userToRulenames, user, rn); }); + this.userToRulesetSymbol.set(user, Symbol()); } delUserGroup(user, group) { delFromMapSet(this.groupToUsers, group, user); + let clearRuleset = false; getMapSet(this.groupToRulenames, group).forEach((rn) => { - delFromMapSet(this.userToRulenames, user, rn); + const deletedEmptySet = delFromMapSet(this.userToRulenames, user, rn); + if (deletedEmptySet) { clearRuleset = true; } }); + if (clearRuleset) { + this.userToRulesetSymbol.delete(user); + } else { + this.userToRulesetSymbol.set(user, Symbol()); + } } addGroupRule(group, ruleName, rule) { @@ -59,6 +86,7 @@ class RuleMap { addToMapSet(this.groupToRulenames, group, ruleName); getMapSet(this.groupToUsers, group).forEach((user) => { addToMapSet(this.userToRulenames, user, ruleName); + this.userToRulesetSymbol.set(user, Symbol()); }); } @@ -67,7 +95,12 @@ class RuleMap { this.rulenameToRule.delete(ruleName); delFromMapSet(this.groupToRulenames, group, ruleName); getMapSet(this.groupToUsers, group).forEach((user) => { - delFromMapSet(this.userToRulenames, user, ruleName); + const deletedEmptySet = delFromMapSet(this.userToRulenames, user, ruleName); + if (deletedEmptySet) { + this.userToRulesetSymbol.delete(user); + } else { + this.userToRulesetSymbol.set(user, Symbol()); + } }); } @@ -76,6 +109,7 @@ class RuleMap { this.groupToRulenames.clear(); this.RulenameToRule.clear(); this.userToRulenames.clear(); + this.userToRulesetSymbol.clear(); } } @@ -175,12 +209,43 @@ class UserCache { } return { - getValidatePromise(req) { + getValidatePromise: (req) => { return cfeed.readyPromise.then(() => { - return (...args) => + let rulesetSymbol = Symbol(); + let ruleset = []; + return (...args) => { + const userStale = cfeed.unreadyAt; + const groupsStale = this.groupsUnreadyAt; + if (userStale || groupsStale) { + let staleSince = null + const curTime = Number(new Date()); + if (userStale && (curTime - Number(userStale) > staleLimit)) { + staleSince = userStale; + } + if (groupsStale && (curTime - Number(groupsStale) > staleLimit)) { + if (!staleSince || Number(groupsStale) < Number(staleSince)) { + staleSince = groupsStale; + } + } + if (staleSince) { + throw new Error(`permissions desynced since ${staleSince}`); + } + } + const curSymbol = this.ruleMap.getUserRulesetSymbol(userId); + if (curSymbol !== rulesetSymbol) { + rulesetSymbol = curSymbol; + ruleset = []; + this.ruleMap.forEachUserRule(userId, (rule) => { + if (rule.isMatch(req.options)) { + ruleset.push(rule); + } + }); + } + // RSI: pick up here, call validator functions. + }; }); }, - close() { + close: () => { }, }; From 359039a5acf5b2e56abe2323935b21569944728d Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Thu, 25 Aug 2016 00:31:02 +0000 Subject: [PATCH 27/86] theoretically working --- plugins/permissions/src/permissions.js | 158 ++++--------------------- 1 file changed, 24 insertions(+), 134 deletions(-) diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 762869c36..4bf9acedc 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -150,6 +150,7 @@ class UserCache { if (cfeed.unreadyAt === undefined) { cfeed.unreadyAt = new Date(0); // epoch } + cfeed.refcount = 0; return cfeed; }; @@ -201,19 +202,21 @@ class UserCache { subscribe(userId) { let cfeed = this.userCfeeds.get(userId); if (!cfeed) { - this.userCfeeds.set(userId, cfeed = newUserCfeed()); + this.userCfeeds.set(userId, cfeed = this.newUserCfeed()); cfeed.readyPromise = new Promise((resolve, reject) => { cfeed.subscribe({onReady: () => resolve()}); setTimeout(() => reject(new Error('timed out')), this.timeout) }); } + cfeed.refcount += 1; return { getValidatePromise: (req) => { return cfeed.readyPromise.then(() => { let rulesetSymbol = Symbol(); let ruleset = []; - return (...args) => { + let needsValidation = true; + return () => { const userStale = cfeed.unreadyAt; const groupsStale = this.groupsUnreadyAt; if (userStale || groupsStale) { @@ -235,18 +238,35 @@ class UserCache { if (curSymbol !== rulesetSymbol) { rulesetSymbol = curSymbol; ruleset = []; + needsValidation = true; this.ruleMap.forEachUserRule(userId, (rule) => { if (rule.isMatch(req.options)) { + if (!rule.validator) { + needsValidation = false; + } ruleset.push(rule); } }); } - // RSI: pick up here, call validator functions. + if (!needsValidation) { + return null; + } + return (...args) => { + for (const rule of ruleset) { + if (rule.is_valid(...args)) { + return rule; + } + } + }; }; }); }, close: () => { - + cfeed.refcount -= 1; + if (cfeed.refcount === 0) { + this.userCfeeds.delete(userId); + return cfeed.close(); + } }, }; } @@ -299,133 +319,3 @@ module.exports = (config) => { }, }; } - -module.exports = (config) => { - class User { - constructor(user_id, reliable_conn) { - this.feed = r.table(config.user_table); - } - - group_changed(group_name) { - if (this.data && this.data.groups && this.data.groups.indexOf(group_name) !== -1) { - this.active_rulesets.forEach((ruleset) => ); - } - } - - add_request(req, res, next) { - // Create a changefeed for the user unless it exists - - // Template-match the request options - - // Add a ruleset to request.context.rules - const ruleset = new Ruleset(); - request.context.rules = ruleset; - this.active_rulesets.add(ruleset); - - const cleanup = () => this.active_rulesets.delete(ruleset); - - // On changes to the rules in any of the user's groups, re-evaluate rules - - // On response completion, stop tracking the ruleset - res.complete.then(cleanup).catch(cleanup); - } - } - - return { - name: 'permissions', - activate: (server) => { - const reliable_conn = server.conn(); - const users = new Map(); - const groups = new Map(); - const ready = false; - - // TODO: need to save/close the subscription? - reliable_conn.subscribe({ - onUnready: (reason) => { - users.forEach((user) => user.close(reason)); - users.clear(); - }, - }); - - // Set up a reliable changefeed on the 'groups' table to track rules by group name - const groups_feed = new ReliableChangefeed( - r.db(server.project_name) - .table('hz_groups') - .changes({squash: false, includeInitial: true, includeTypes: true}), - reliable_conn, - { - onReady: () => { - ready = true; - }, - onUnready: () => { - ready = false; - groups.forEach((g) => g.close()); - groups.clear(); - }, - onChange: (change) => { - switch(change.type) { - 'initial': - 'add': - 'change': - { - const group = new Group(change.new_val); - groups.set(group.name, group); - users.forEach((user) => user.group_changed(group.name)); - } - break; - 'uninitial': - 'remove': - { - const name = change.old_val.id; - const group = groups.delete(change.old_val.id); - } - break; - default: - // RSI: log error - break; - } - }, - }); - - const get_user = (user_id) => { - let user = users.get(user_id); - if (!user) { - user = new User(user_id, reliable_conn); - users.set(user_id, user); - } - return user; - }; - - ctx.logger.info('Activating permissions.'); - return { - deactivate: (reason) => { - user_feeds.forEach((feed) => feed.close(reason)); - user_feeds.clear(); - }, - methods: { - 'permissions': { - type: 'prereq', - run: (req, res, next) => { - if (!ready) { - throw new Error( - 'Groups are not synced with the server, cannot validate requests.'); - } - - const user_id = request.context.user_id; - if (user_id !== undefined) { - throw new Error('Client has not been authenticated'); - } - - // Find the user and register this request - const user = get_user(user_id); - user.add_request(req, res, next); - }, - } - }, - }; - }, - }; -} - -module.exports.validate = (rules, context, ...args) => { -} From 13d61096e88efe75afc362e1dabee39ec6a067fd Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 24 Aug 2016 23:15:08 -0700 Subject: [PATCH 28/86] moved logic from server to permissions plugin --- plugins/permissions/package.json | 6 +- plugins/permissions/src/group.js | 13 ++ plugins/permissions/src/permissions.js | 66 ++++--- plugins/permissions/src/remake_error.js | 11 ++ plugins/permissions/src/rule.js | 26 +++ plugins/permissions/src/template.js | 249 ++++++++++++++++++++++++ plugins/permissions/src/validator.js | 23 +++ 7 files changed, 365 insertions(+), 29 deletions(-) create mode 100644 plugins/permissions/src/group.js create mode 100644 plugins/permissions/src/remake_error.js create mode 100644 plugins/permissions/src/rule.js create mode 100644 plugins/permissions/src/template.js create mode 100644 plugins/permissions/src/validator.js diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index 672c7e854..91cec4c06 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -5,7 +5,7 @@ "main": "src/permissions.js", "scripts": { "lint": "eslint src test", - "test": "mocha test", + "test": "mocha test" }, "repository": { "type": "git", @@ -16,7 +16,7 @@ "bugs": { "url": "https://github.com/rethinkdb/horizon/issues" }, - "homepage": "https://github.com/rethinkdb/horizon#readme" + "homepage": "https://github.com/rethinkdb/horizon#readme", "engines": { "node": ">=4.0.0" }, @@ -25,6 +25,6 @@ "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", + "mocha": "^2.3.3" } } diff --git a/plugins/permissions/src/group.js b/plugins/permissions/src/group.js new file mode 100644 index 000000000..b342afb7b --- /dev/null +++ b/plugins/permissions/src/group.js @@ -0,0 +1,13 @@ +'use strict'; + +const Rule = require('./rule'); + +class Group { + constructor(row_data) { + this.name = row_data.id; + this.rules = Object.keys(row_data.rules).map((name) => + new Rule(name, row_data.rules[name])); + } +} + +module.exports = Group; diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 4bf9acedc..8270c2264 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -1,5 +1,9 @@ 'use strict'; +const Rule = require('./rule'); + +const assert = require('assert'); + // We can be desynced from the database for up to 5 seconds before we // start rejecting queries. const staleLimit = 5000; @@ -8,7 +12,7 @@ const staleLimit = 5000; // token/anonymous: this should be the user id // unauthenticated: this should be null, and will use the default rules -addToMapSet(map, name, el) { +function addToMapSet(map, name, el) { let set = map.get(name); if (!set) { set = new Set(); @@ -20,8 +24,8 @@ addToMapSet(map, name, el) { } // Returns whether or not it deleted an empty set from the map. -delFromMapSet(map, name, el) { - let set = map.get(name); +function delFromMapSet(map, name, el) { + const set = map.get(name); assert(set, `delFromMapSet: ${name} not in map`); assert(set.has(el), `delFromMapSet: ${name} does not have ${el}`); set.delete(el); @@ -32,7 +36,7 @@ delFromMapSet(map, name, el) { return false; } -getMapSet(map, name) { +function getMapSet(map, name) { return map.get(name) || new Set(); } @@ -40,7 +44,7 @@ const emptyRulesetSymbol = Symbol(); class RuleMap { constructor() { this.groupToRulenames = new Map(); - this.RulenameToRule = new Map(); + this.rulenameToRule = new Map(); this.groupToUsers = new Map(); this.userToRulenames = new Map(); // computed @@ -53,7 +57,7 @@ class RuleMap { forEachUserRule(user, cb) { this.userToRulenames.forEach((rn) => { - const rule = this.RulenameToRule.get(rn); + const rule = this.rulenameToRule.get(rn); assert(rule); cb(rule); }); @@ -82,7 +86,7 @@ class RuleMap { } addGroupRule(group, ruleName, rule) { - this.RulenameToRule.set(ruleName, rule); + this.rulenameToRule.set(ruleName, rule); addToMapSet(this.groupToRulenames, group, ruleName); getMapSet(this.groupToUsers, group).forEach((user) => { addToMapSet(this.userToRulenames, user, ruleName); @@ -91,7 +95,7 @@ class RuleMap { } delGroupRule(group, ruleName) { - assert(rulenameToRule.has(ruleName), `unrecognized ${group} rule ${ruleName}`); + assert(this.rulenameToRule.has(ruleName), `unrecognized ${group} rule ${ruleName}`); this.rulenameToRule.delete(ruleName); delFromMapSet(this.groupToRulenames, group, ruleName); getMapSet(this.groupToUsers, group).forEach((user) => { @@ -107,7 +111,7 @@ class RuleMap { // This should be equivalent to calling `delGroupRule` for all rules. delAllGroupRules() { this.groupToRulenames.clear(); - this.RulenameToRule.clear(); + this.rulenameToRule.clear(); this.userToRulenames.clear(); this.userToRulesetSymbol.clear(); } @@ -115,15 +119,17 @@ class RuleMap { class UserCache { constructor(config, ctx) { + const r = ctx.r; + + this.ctx = ctx; this.timeout = config.cacheTimeout; this.ruleMap = new RuleMap(); - this.groupsUnreadyAt = new Date(0); // epoch this.userCfeeds = new Map(); this.newUserCfeed = (userId) => { let oldGroups = new Set(); - const cfeed = new ReliableChangefeed( + const cfeed = new ctx.ReliableChangefeed( r.table(config.usersTable).get(userId).changes({includeInitial: true}), ctx.reliableConn, { @@ -154,12 +160,14 @@ class UserCache { return cfeed; }; - this.groupCfeed = new ReliableChangefeed( + this.queuedGroups = new Map(); + this.groupsUnreadyAt = new Date(0); // epoch + this.groupCfeed = new ctx.ReliableChangefeed( r.table(config.groupsTable).changes({includeInitial: true}), ctx.reliableConn, { onReady: () => { - this.ruleMap.delAllGroupRules(), + this.ruleMap.delAllGroupRules(); this.queuedGroups.forEach( (rules, groupId) => rules.forEach( (rule) => this.ruleMap.addGroupRule( @@ -177,7 +185,7 @@ class UserCache { onChange: (change) => { const id = change.old_val ? change.old_val.id : change.new_val.id; if (this.groupsUnreadyAt !== null) { - queuedGroups.set(id, change.rules); + this.queuedGroups.set(id, change.rules); } else { const oldRules = change.old_val ? change.old_val.rules : {}; const newRules = change.new_val ? change.new_val.rules : {}; @@ -205,14 +213,14 @@ class UserCache { this.userCfeeds.set(userId, cfeed = this.newUserCfeed()); cfeed.readyPromise = new Promise((resolve, reject) => { cfeed.subscribe({onReady: () => resolve()}); - setTimeout(() => reject(new Error('timed out')), this.timeout) + setTimeout(() => reject(new Error('timed out')), this.timeout); }); } cfeed.refcount += 1; return { - getValidatePromise: (req) => { - return cfeed.readyPromise.then(() => { + getValidatePromise: (req) => + cfeed.readyPromise.then(() => { let rulesetSymbol = Symbol(); let ruleset = []; let needsValidation = true; @@ -220,7 +228,7 @@ class UserCache { const userStale = cfeed.unreadyAt; const groupsStale = this.groupsUnreadyAt; if (userStale || groupsStale) { - let staleSince = null + let staleSince = null; const curTime = Number(new Date()); if (userStale && (curTime - Number(userStale) > staleLimit)) { staleSince = userStale; @@ -252,15 +260,21 @@ class UserCache { return null; } return (...args) => { - for (const rule of ruleset) { - if (rule.is_valid(...args)) { - return rule; + try { + for (const rule of ruleset) { + if (rule.is_valid(...args)) { + return rule; + } } + } catch (err) { + // We don't want to pass the error message on to the user because + // it might leak information about the data. + this.ctx.logger.error(`Exception in validator function: ${err.stack}`); + throw new Error('Validation error'); } }; }; - }); - }, + }), close: () => { cfeed.refcount -= 1; if (cfeed.refcount === 0) { @@ -274,7 +288,7 @@ class UserCache { // RSI: remove the extra level of function calling. module.exports = (config) => { - const name = config.name || 'permissions', + const name = config.name || 'permissions'; const userCache = Symbol(`${name}_userCache`); const userSub = Symbol(`${name}_userSub`); return { @@ -285,7 +299,7 @@ module.exports = (config) => { ctx[userCache] = new UserCache(config, ctx); return { methods: { - 'hz_permissions': { + hz_permissions: { type: 'preReq', handler: (req, res, next) => { if (!req.clientCtx[userSub]) { @@ -318,4 +332,4 @@ module.exports = (config) => { } }, }; -} +}; diff --git a/plugins/permissions/src/remake_error.js b/plugins/permissions/src/remake_error.js new file mode 100644 index 000000000..9ec5ec5ef --- /dev/null +++ b/plugins/permissions/src/remake_error.js @@ -0,0 +1,11 @@ +'use strict'; + +// Used when evaluating things in a different VM context - the errors +// thrown from there will not evaluate as `instanceof Error`, so we recreate them. +const remake_error = (err) => { + const new_err = new Error(err.message || 'Unknown error when evaluating template.'); + new_err.stack = err.stack || new_err.stack; + throw new_err; +}; + +module.exports = remake_error; diff --git a/plugins/permissions/src/rule.js b/plugins/permissions/src/rule.js new file mode 100644 index 000000000..e82755bcb --- /dev/null +++ b/plugins/permissions/src/rule.js @@ -0,0 +1,26 @@ +'use strict'; +const Template = require('./template').Template; +const Validator = require('./validator').Validator; + +class Rule { + constructor(name, info) { + this.name = name; + this.template = new Template(info.template); + if (info.validator) { + this.validator = new Validator(info.validator); + } + } + + is_match(query, context) { + return this._template.is_match(query, context); + } + + is_valid(...args) { + if (!this._validator) { + return true; + } + return this._validator.is_valid(...args); + } +} + +module.exports = Rule; diff --git a/plugins/permissions/src/template.js b/plugins/permissions/src/template.js new file mode 100644 index 000000000..caa530c9e --- /dev/null +++ b/plugins/permissions/src/template.js @@ -0,0 +1,249 @@ +'use strict'; + +const remake_error = require('./remake_error'); + +const assert = require('assert'); +const vm = require('vm'); + +const ast = require('@horizon/client/lib/ast'); +const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; + +class Any { + constructor(values) { + this._values = values || []; + } + + matches(value, context) { + if (value === undefined) { + return false; + } else if (this._values.length === 0) { + return true; + } + + for (const item of this._values) { + if (template_compare(value, item, context)) { + return true; + } + } + + return false; + } +} + +// This works the same as specifying a literal object in a template, except that +// unspecified key/value pairs are allowed. +class AnyObject { + constructor(obj) { + this._obj = obj || { }; + } + + matches(value, context) { + if (value === null || typeof value !== 'object') { + return false; + } + + for (const key in this._obj) { + if (!template_compare(value[key], this._obj[key], context)) { + return false; + } + } + + return true; + } +} + +// This matches an array where each item matches at least one of the values +// specified at construction. +class AnyArray { + constructor(values) { + this._values = values || []; + } + + matches(value, context) { + if (!Array.isArray(value)) { + return false; + } + + for (const item of value) { + let match = false; + for (const template of this._values) { + if (template_compare(item, template, context)) { + match = true; + break; + } + } + if (!match) { + return false; + } + } + + return true; + } +} + +class UserId { } + +const wrap_write = (query, docs) => { + if (docs instanceof AnyArray || + Array.isArray(docs)) { + query.data = docs; + } else { + query.data = [docs]; + } + return query; +}; + +const wrap_remove = (doc) => { + if (validIndexValue(doc)) { + return {id: doc}; + } + return doc; +}; + +// Add helper methods to match any subset of the current query for reads or writes +ast.TermBase.prototype.anyRead = function() { + return this._sendRequest(new Any(['query', 'subscribe']), + new AnyObject(this._query)); +}; + +ast.Collection.prototype.anyWrite = function() { + let docs = arguments; + if (arguments.length === 0) { + docs = new AnyArray(new Any()); + } + return this._sendRequest( + new Any(['store', 'upsert', 'insert', 'replace', 'update', 'remove']), + wrap_write(new AnyObject(this._query), docs)); +}; + +// Monkey-patch the ast functions so we don't clobber certain things +ast.TermBase.prototype.watch = function() { + return this._sendRequest('subscribe', this._query); +}; +ast.TermBase.prototype.fetch = function() { + return this._sendRequest('query', this._query); +}; +ast.Collection.prototype.store = function(docs) { + return this._sendRequest('store', wrap_write(this._query, docs)); +}; +ast.Collection.prototype.upsert = function(docs) { + return this._sendRequest('upsert', wrap_write(this._query, docs)); +}; +ast.Collection.prototype.insert = function(docs) { + return this._sendRequest('insert', wrap_write(this._query, docs)); +}; +ast.Collection.prototype.replace = function(docs) { + return this._sendRequest('replace', wrap_write(this._query, docs)); +}; +ast.Collection.prototype.update = function(docs) { + return this._sendRequest('update', wrap_write(this._query, docs)); +}; +ast.Collection.prototype.remove = function(doc) { + return this._sendRequest('remove', wrap_write(this._query, wrap_remove(doc))); +}; +ast.Collection.prototype.removeAll = function(docs) { + return this._sendRequest('remove', wrap_write(this._query, + docs.map((doc) => wrap_remove(doc)))); +}; + +const env = { + collection: (name) => new ast.Collection((type, options) => + ({request_id: new Any(), + type: Array.isArray(type) ? new Any(type) : type, + options}), name, false), + any: function() { return new Any(Array.from(arguments)); }, + anyObject: function(obj) { return new AnyObject(obj); }, + anyArray: function() { return new AnyArray(Array.from(arguments)); }, + userId: function() { return new UserId(); }, +}; + +const make_template = (str) => { + try { + const sandbox = Object.assign({}, env); + return vm.runInNewContext(str, sandbox); + } catch (err) { + throw remake_error(err); + } +}; + +// eslint-disable-next-line prefer-const +function template_compare(query, template, context) { + if (template === undefined) { + return false; + } else if (template instanceof Any || + template instanceof AnyObject || + template instanceof AnyArray) { + if (!template.matches(query, context)) { + return false; + } + } else if (template instanceof UserId) { + if (query !== context.id) { + return false; + } + } else if (template === null) { + if (query !== null) { + return false; + } + } else if (Array.isArray(template)) { + if (!Array.isArray(query) || + template.length !== query.length) { + return false; + } + for (let i = 0; i < template.length; ++i) { + if (!template_compare(query[i], template[i], context)) { + return false; + } + } + } else if (typeof template === 'object') { + if (typeof query !== 'object') { + return false; + } + + for (const key in query) { + if (!template_compare(query[key], template[key], context)) { + return false; + } + } + + // Make sure all template keys were handled + for (const key in template) { + if (query[key] === undefined) { + return false; + } + } + } else if (template !== query) { + return false; + } + + return true; +} + +const incomplete_template_message = (str) => + `Incomplete template "${str}", ` + + 'consider adding ".fetch()", ".watch()", ".anyRead()", or ".anyWrite()"'; + +class Template { + constructor(str) { + this._value = make_template(str); + assert(this._value !== null, `Invalid template: ${str}`); + assert(!Array.isArray(this._value), `Invalid template: ${str}`); + assert(typeof this._value === 'object', `Invalid template: ${str}`); + if (!(this._value instanceof Any) && !(this._value instanceof AnyObject)) { + if (this._value.request_id === undefined && + this._value.type === undefined && + this._value.options === undefined && + this._value.anyRead) { + this._value = this._value.anyRead(); + } + assert(this._value.request_id !== undefined, incomplete_template_message(str)); + assert(this._value.type !== undefined, incomplete_template_message(str)); + assert(this._value.options !== undefined, incomplete_template_message(str)); + } + } + + is_match(raw_query, context) { + return template_compare(raw_query, this._value, context); + } +} + +module.exports = {Template}; diff --git a/plugins/permissions/src/validator.js b/plugins/permissions/src/validator.js new file mode 100644 index 000000000..933118d36 --- /dev/null +++ b/plugins/permissions/src/validator.js @@ -0,0 +1,23 @@ +'use strict'; + +const remake_error = require('./remake_error'); + +const assert = require('assert'); +const vm = require('vm'); + +class Validator { + constructor(str) { + try { + this._fn = vm.runInNewContext(str, { }); + } catch (err) { + throw remake_error(err); + } + assert(typeof this._fn === 'function'); + } + + is_valid(...args) { + return this._fn(...args); + } +} + +module.exports = Validator; From 75f96a97eb5f6d6acd269985ce323dc9d364dc77 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 24 Aug 2016 23:27:01 -0700 Subject: [PATCH 29/86] moving horizon.js to index.js --- server/package.json | 2 +- server/src/{horizon.js => index.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename server/src/{horizon.js => index.js} (100%) diff --git a/server/package.json b/server/package.json index ae777a147..eb72c1253 100644 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,7 @@ "name": "@horizon/server", "version": "2.0.0-beta-5", "description": "Server for RethinkDB Horizon, an open-source developer platform for building realtime, scalable web apps.", - "main": "dist/horizon.js", + "main": "dist/index.js", "scripts": { "lint": "eslint src test", "test": "mocha dist/test --timeout 10000", diff --git a/server/src/horizon.js b/server/src/index.js similarity index 100% rename from server/src/horizon.js rename to server/src/index.js From 9ef28ae510db23f3ed55a9c2ea9b73a7e578adb3 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 24 Aug 2016 23:54:44 -0700 Subject: [PATCH 30/86] checkpoint --- plugins/permissions/src/permissions.js | 8 ++- server/.eslintrc.js | 1 + server/src/client.js | 92 ++++++++------------------ server/src/server.js | 65 ++++++++++++------ 4 files changed, 76 insertions(+), 90 deletions(-) diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 8270c2264..d82d0c410 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -6,12 +6,14 @@ const assert = require('assert'); // We can be desynced from the database for up to 5 seconds before we // start rejecting queries. -const staleLimit = 5000; +const staleMs = 5000; // auth plugins should set 'request.context.user' // token/anonymous: this should be the user id // unauthenticated: this should be null, and will use the default rules +// RSI: do something drastic when a user's account is deleted + function addToMapSet(map, name, el) { let set = map.get(name); if (!set) { @@ -230,10 +232,10 @@ class UserCache { if (userStale || groupsStale) { let staleSince = null; const curTime = Number(new Date()); - if (userStale && (curTime - Number(userStale) > staleLimit)) { + if (userStale && (curTime - Number(userStale) > staleMs)) { staleSince = userStale; } - if (groupsStale && (curTime - Number(groupsStale) > staleLimit)) { + if (groupsStale && (curTime - Number(groupsStale) > staleMs)) { if (!staleSince || Number(groupsStale) < Number(staleSince)) { staleSince = groupsStale; } diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 535101a8a..98faeb27b 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -5,6 +5,7 @@ const ERROR = 2; module.exports = { extends: "../.eslintrc.js", rules: { + "camelcase": [ ERROR ], "max-len": [ ERROR, 130 ], "prefer-template": [ OFF ], }, diff --git a/server/src/client.js b/server/src/client.js index ededb4a8a..181c78204 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -7,22 +7,19 @@ const Response = require('./response').Response; const Joi = require('joi'); const websocket = require('ws'); -class Client { - constructor(socket, auth, reliable_metadata, - auth_middleware_cb, request_middleware_cb) { - if (!reliable_metadata.ready) { - throw new Error('No connection to the database.'); - } - - logger.debug('Client connection established.'); +class ClientConnection { + constructor(socket, + auth, + middlewareCb, + clientEvents) { + logger.debug('ClientConnection established.'); this.socket = socket; this.auth = auth; - this.reliable_metadata = reliable_metadata; - this.auth_middleware_cb = auth_middleware_cb; - this.request_middleware_cb = request_middleware_cb; + this.middlewareCb = middlewareCb; + this.clientEvents = clientEvents; + this.context = { }; this.responses = new Map(); - this.user_info = { }; this.socket.on('close', (code, msg) => this.handle_websocket_close(code, msg)); @@ -35,12 +32,12 @@ class Client { this.error_wrap_socket(() => this.handle_handshake(data))); } + context() { + return this.context; + } + handle_websocket_close() { - logger.debug('Client connection terminated.'); - // RSI: move to permissions? - if (this.user_feed) { - this.user_feed.close().catch(() => { }); - } + logger.debug('ClientConnection closed.'); this.responses.forEach((response) => response.close()); this.responses.clear(); } @@ -90,13 +87,6 @@ class Client { } } - // RSI: move to permissions plugin - group_changed(group_name) { - if (this.user_info.groups.indexOf(group_name) !== -1) { - this.responses.forEach((response) => response.evaluate_rules()); - } - } - handle_handshake(data) { const request = this.parse_request(data, schemas.handshake); logger.debug(`Received handshake: ${JSON.stringify(request)}`); @@ -105,47 +95,19 @@ class Client { return this.close({error: 'Invalid handshake.', error_code: 0}); } - let responded = false; this.auth.handshake(request).then((res) => { - const finish_handshake = () => { - if (!responded) { - responded = true; - const info = { - token: res.token, - id: res.payload.id, - provider: res.payload.provider, - }; - this.send_message(request, info); - this.socket.on('message', (msg) => - this.error_wrap_socket(() => this.handle_request(msg))); - } + this.context.user = res.payload; + const info = { + token: res.token, + id: res.payload.id, + provider: res.payload.provider, }; - this.user_info = res.payload; - - if (this.user_info.id != null) { - // RSI: move to permissions plugin - return this.metadata.get_user_feed(this.user_info.id).then((feed) => { - this.user_feed = feed; - return feed.eachAsync((change) => { - if (!change.new_val) { - throw new Error('User account has been deleted.'); - } - Object.assign(this.user_info, change.new_val); - this.responses.forEach((response) => response.evaluate_rules()); - finish_handshake(); - }).then(() => { - throw new Error('User account feed has been lost.'); - }); - }); - } else { - this.user_info.groups = ['default']; - finish_handshake(); - } + this.send_message(request, info); + this.socket.on('message', (msg) => + this.error_wrap_socket(() => this.handle_request(msg))); + this.clientEvents.emit('auth', this.context); }).catch((err) => { - if (!responded) { - responded = true; - this.close({request_id: request.request_id, error: `${err}`, error_code: 0}); - } + this.close({request_id: request.request_id, error: `${err}`, error_code: 0}); }); } @@ -164,7 +126,7 @@ class Client { const response = new Response((obj) => this.send_message(raw_request, obj)); this.responses.set(raw_request.request_id, response); - this.request_middleware_cb(raw_request, response); + this.middlewareCb(raw_request, response); } @@ -184,7 +146,7 @@ class Client { if (this.is_open()) { const close_msg = (info.error && info.error.substr(0, 64)) || 'Unspecified reason.'; - logger.debug('Closing client connection with message: ' + + logger.debug('Closing ClientConnection with message: ' + `${info.error || 'Unspecified reason.'}`); logger.debug(`info: ${JSON.stringify(info)}`); if (info.request_id !== undefined) { @@ -213,4 +175,4 @@ class Client { } } -module.exports = {Client}; +module.exports = ClientConnection; diff --git a/server/src/server.js b/server/src/server.js index 2a550e0c1..117e778cd 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -1,15 +1,16 @@ 'use strict'; const Auth = require('./auth').Auth; -const Client = require('./client').Client; +const ClientConnection = require('./client'); const logger = require('./logger'); -const {ReliableConn} = require('./reliable'); +const {ReliableConn, ReliableChangefeed} = require('./reliable'); const {ReliableMetadata} = require('./metadata/reliable_metadata'); const options_schema = require('./schema/server_options').server; const EventEmitter = require('events'); const Joi = require('joi'); const websocket = require('ws'); +const r = require('rethinkdb'); const protocol_name = 'rethinkdb-horizon-v0'; @@ -31,12 +32,13 @@ class Server extends EventEmitter { this._request_handlers = new Map(); this._ws_servers = []; this._close_promise = null; - this._default_middleware = (req, res, next) => { + this._defaultMiddlewareCb = (req, res, next) => { next(new Error('No middleware to handle the request.')); }; - this._middleware = this._default_middleware; + this._middlewareCb = this._defaultMiddlewareCb; + this._auth = new Auth(this, opts.auth); - this._reliable_conn = new ReliableConn({ + this._reliableConn = new ReliableConn({ host: opts.rdb_host, port: opts.rdb_port, db: opts.project_name, @@ -45,16 +47,28 @@ class Server extends EventEmitter { timeout: opts.rdb_timeout || null, }); this._clients = new Set(); + this._clientEvents = new EventEmitter(); + + // server context passed to plugins when activated + this._pluginContext = { + r, + opts, + logger, + ReliableChangefeed, + auth: this._auth, + clientEvents: this._clientEvents, + reliableConn: this._reliableConn, + }; // TODO: consider emitting errors sometimes. - this._reliable_metadata = new ReliableMetadata( + this._reliableMetadata = new ReliableMetadata( opts.project_name, - this._reliable_conn, + this._reliableConn, this._clients, opts.auto_create_collection, opts.auto_create_index); - this._clear_clients_subscription = this._reliable_metadata.subscribe({ + this._clear_clients_subscription = this._reliableMetadata.subscribe({ onReady: () => { this.emit('ready', this); }, @@ -66,11 +80,9 @@ class Server extends EventEmitter { }, }); - this._auth = new Auth(this, opts.auth); - const verifyClient = (info, cb) => { // Reject connections if we aren't synced with the database - if (!this._reliable_metadata.ready) { + if (!this._reliableMetadata.ready) { cb(false, 503, 'Connection to the database is down.'); } else { cb(true); @@ -85,14 +97,19 @@ class Server extends EventEmitter { .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => { try { - const client = new Client( - socket, - this._auth, - this._reliable_metadata, - this._middleware + if (!this._reliableMetadata.ready) { + throw new Error('No connection to the database.'); + } + + const client = new ClientConnection( + socket, this._auth, this._middlewareCb, this._clientEvents ); this._clients.add(client); - socket.on('close', () => this._clients.delete(client)); + this._clientEvents.emit('connect', client.context()); + socket.on('close', () => { + this._clients.delete(client); + this._clientEvents.emit('disconnect', client.context()); + }); } catch (err) { logger.error(`Failed to construct client: ${err}`); if (socket.readyState === websocket.OPEN) { @@ -111,28 +128,32 @@ class Server extends EventEmitter { } } + context() { + return this._pluginContext; + } + metadata() { - return this._reliable_metadata; + return this._reliableMetadata; } conn() { - return this._reliable_conn; + return this._reliableConn; } set_middleware(mw) { - this._middleware = mw ? mw : this._default_middleware; + this._middlewareCb = mw ? mw : this._defaultMiddlewareCb; } // TODO: We close clients in `onUnready` above, but don't wait for // them to be closed. close() { if (!this._close_promise) { - this._close_promise = this._reliable_metadata.close().then( + this._close_promise = this._reliableMetadata.close().then( () => Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { s.close(resolve); }))) ).then( - () => this._reliable_conn.close() + () => this._reliableConn.close() ); } return this._close_promise; From 4bf76e855d6cfbaf1ae1a4013ab28123df2c0d35 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 24 Aug 2016 23:57:17 -0700 Subject: [PATCH 31/86] camelcase changes --- server/src/server.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/server.js b/server/src/server.js index 117e778cd..2034ec248 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -5,20 +5,20 @@ const ClientConnection = require('./client'); const logger = require('./logger'); const {ReliableConn, ReliableChangefeed} = require('./reliable'); const {ReliableMetadata} = require('./metadata/reliable_metadata'); -const options_schema = require('./schema/server_options').server; +const optionsSchema = require('./schema/server_options').server; const EventEmitter = require('events'); const Joi = require('joi'); const websocket = require('ws'); const r = require('rethinkdb'); -const protocol_name = 'rethinkdb-horizon-v0'; +const protocolName = 'rethinkdb-horizon-v0'; function handleProtocols(protocols, cb) { - if (protocols.findIndex((x) => x === protocol_name) !== -1) { - cb(true, protocol_name); + if (protocols.findIndex((x) => x === protocolName) !== -1) { + cb(true, protocolName); } else { - logger.debug(`Rejecting client without "${protocol_name}" protocol (${protocols}).`); + logger.debug(`Rejecting client without "${protocolName}" protocol (${protocols}).`); cb(false, null); } } @@ -26,7 +26,7 @@ function handleProtocols(protocols, cb) { class Server extends EventEmitter { constructor(http_servers, user_opts) { super(); - const opts = Joi.attempt(user_opts || { }, options_schema); + const opts = Joi.attempt(user_opts || { }, optionsSchema); this._original_user_opts = user_opts; this._auth_methods = { }; this._request_handlers = new Map(); @@ -162,5 +162,5 @@ class Server extends EventEmitter { module.exports = { Server, - protocol: protocol_name, + protocol: protocolName, }; From 0c9f57a1b6f991e3475d2ad1a31839c6ac60c99f Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 25 Aug 2016 00:10:09 -0700 Subject: [PATCH 32/86] checkpoint --- server/src/client.js | 2 +- server/src/response.js | 90 +----------------------------------------- server/src/server.js | 31 ++++++--------- server/src/utils.js | 9 ----- 4 files changed, 14 insertions(+), 118 deletions(-) diff --git a/server/src/client.js b/server/src/client.js index 181c78204..cc5a6f208 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -2,7 +2,7 @@ const logger = require('./logger'); const schemas = require('./schema/horizon_protocol'); -const Response = require('./response').Response; +const Response = require('./response'); const Joi = require('joi'); const websocket = require('ws'); diff --git a/server/src/response.js b/server/src/response.js index 08edec2f9..258f0dcfb 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -1,10 +1,6 @@ 'use strict'; -const logger = require('./logger'); -const rule = require('./permissions/rule'); - class Response { - // RSI: make sure socketSend sets `request_id`. constructor(socketSend) { this._socketSend = socketSend; this.complete = new Promise((resolve, reject) => { @@ -43,88 +39,4 @@ class Response { } } -class Request { - constructor(raw_request, endpoint, client) { - this._raw_request = raw_request; - this._ruleset = new rule.Ruleset(); - this._endpoint = endpoint; - this._client = client; - this.evaluate_rules(); - } - - evaluate_rules() { - if (this._client._permissions_enabled) { - const metadata = this._client._metadata; - const user_info = this._client.user_info; - const matching_rules = []; - for (const group_name of user_info.groups) { - const group = metadata.get_group(group_name); - if (group !== undefined) { - for (const r of group.rules) { - if (r.is_match(this._raw_request, user_info)) { - matching_rules.push(r); - } - } - } - } - this._ruleset.update(matching_rules); - } else { - this._ruleset.update([rule.any_rule]); - } - } - - run() { - let complete = false; - try { - if (this._ruleset.empty()) { - throw new Error('Operation not permitted.'); - } - this._cancel_cb = this._endpoint(this._raw_request, - this._client.user_info, - this._ruleset, - this._client._metadata, - (res) => { - this._client.send_response(this._raw_request, res); - }, - (res) => { - // Only send something the first time 'done' is called - if (!complete) { - complete = true; - if (res instanceof Error) { - this.handle_error(res); - } else if (res) { - this._client.send_response(this._raw_request, res); - } - this._client.remove_request(this._raw_request); - } - }); - } catch (err) { - this.handle_error(err); - } - } - - - close() { - this._ruleset.clear(); - if (this._cancel_cb) { - this._cancel_cb(); - } - } - - handle_error(err) { - logger.debug(`Error on request ${this._raw_request.request_id}:\n${err.stack}`); - - // Ignore errors for disconnected clients - if (this._client.is_open()) { - this._client._metadata.handle_error(err, (inner_err) => { - if (inner_err) { - this._client.send_error(this._raw_request, inner_err); - } else { - setImmediate(() => this.run()); - } - }); - } - } -} - -module.exports = {Response, Request}; +module.exports = Response; diff --git a/server/src/server.js b/server/src/server.js index 2034ec248..ee021b25d 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -26,8 +26,7 @@ function handleProtocols(protocols, cb) { class Server extends EventEmitter { constructor(http_servers, user_opts) { super(); - const opts = Joi.attempt(user_opts || { }, optionsSchema); - this._original_user_opts = user_opts; + this.options = Joi.attempt(user_opts || { }, optionsSchema); this._auth_methods = { }; this._request_handlers = new Map(); this._ws_servers = []; @@ -47,18 +46,9 @@ class Server extends EventEmitter { timeout: opts.rdb_timeout || null, }); this._clients = new Set(); - this._clientEvents = new EventEmitter(); - - // server context passed to plugins when activated - this._pluginContext = { - r, - opts, - logger, - ReliableChangefeed, - auth: this._auth, - clientEvents: this._clientEvents, - reliableConn: this._reliableConn, - }; + + this.r = r; + this.logger = logger; // TODO: consider emitting errors sometimes. this._reliableMetadata = new ReliableMetadata( @@ -102,13 +92,16 @@ class Server extends EventEmitter { } const client = new ClientConnection( - socket, this._auth, this._middlewareCb, this._clientEvents + socket, + this._auth, + this._middlewareCb, + this // Used to emit a client auth event ); this._clients.add(client); - this._clientEvents.emit('connect', client.context()); + this.emit('connect', client.context()); socket.on('close', () => { this._clients.delete(client); - this._clientEvents.emit('disconnect', client.context()); + this.emit('disconnect', client.context()); }); } catch (err) { logger.error(`Failed to construct client: ${err}`); @@ -128,8 +121,8 @@ class Server extends EventEmitter { } } - context() { - return this._pluginContext; + auth() { + return this._auth; } metadata() { diff --git a/server/src/utils.js b/server/src/utils.js index 9927c5405..1b79966f1 100644 --- a/server/src/utils.js +++ b/server/src/utils.js @@ -34,15 +34,6 @@ const rethinkdb_version_check = (version_string) => { } }; -// Used when evaluating things in a different VM context - the errors -// thrown from there will not evaluate as `instanceof Error`, so we recreate them. -const remake_error = (err) => { - const new_err = new Error(err.message || 'Unknown error when evaluating template.'); - new_err.stack = err.stack || new_err.stack; - throw new_err; -}; - module.exports = { rethinkdb_version_check, - remake_error, }; From b8d4a086511d4c9372757b1a42dac9d5cf1316b6 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 25 Aug 2016 00:13:52 -0700 Subject: [PATCH 33/86] checkpoint --- server/src/error.js | 7 +- server/src/metadata/index.js | 2 +- server/src/metadata/reliable_metadata.js | 4 +- server/src/metadata/table.js | 3 +- server/src/permissions/group.js | 11 - server/src/permissions/rule.js | 68 ------- server/src/permissions/template.js | 247 ----------------------- server/src/permissions/validator.js | 32 --- server/src/reql_connection.js | 130 ------------ 9 files changed, 7 insertions(+), 497 deletions(-) delete mode 100644 server/src/permissions/group.js delete mode 100644 server/src/permissions/rule.js delete mode 100644 server/src/permissions/template.js delete mode 100644 server/src/permissions/validator.js delete mode 100644 server/src/reql_connection.js diff --git a/server/src/error.js b/server/src/error.js index 6a102eccb..c9fa0b7fd 100644 --- a/server/src/error.js +++ b/server/src/error.js @@ -1,10 +1,6 @@ 'use strict'; -const check = (pred, message) => { - if (!pred) { - throw new Error(message); - } -}; +const assert = require('assert'); const fail = (message) => check(false, message); @@ -39,7 +35,6 @@ class CollectionNotReady extends Error { } module.exports = { - check, fail, IndexMissing, IndexNotReady, diff --git a/server/src/metadata/index.js b/server/src/metadata/index.js index a8744b127..aa9747161 100644 --- a/server/src/metadata/index.js +++ b/server/src/metadata/index.js @@ -1,6 +1,6 @@ 'use strict'; -const check = require('../error').check; +const assert = require('assert'); const logger = require('../logger'); // Index names are of the format "hz_[_]" where may be diff --git a/server/src/metadata/reliable_metadata.js b/server/src/metadata/reliable_metadata.js index 2a85dfa55..329aeeefc 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/server/src/metadata/reliable_metadata.js @@ -6,6 +6,8 @@ const logger = require('../logger'); const Collection = require('./collection').Collection; const utils = require('../utils'); +const assert = require('assert'); + const r = require('rethinkdb'); const version_field = '$hz_v$'; @@ -333,7 +335,7 @@ class ReliableMetadata extends Reliable { this._collections.set(name, collection); create_collection(this._db, name, this._reliable_conn.connection()).then((res) => { - error.check(!res.error, `Collection "${name}" creation failed: ${res.error}`); + assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); logger.warn(`Collection created: "${name}"`); collection._on_ready(done); }).catch((err) => { diff --git a/server/src/metadata/table.js b/server/src/metadata/table.js index e218cb658..82afca07a 100644 --- a/server/src/metadata/table.js +++ b/server/src/metadata/table.js @@ -1,9 +1,10 @@ 'use strict'; -const error = require('../error'); const index = require('./index'); const logger = require('../logger'); +const assert = require('assert'); + const r = require('rethinkdb'); class Table { diff --git a/server/src/permissions/group.js b/server/src/permissions/group.js deleted file mode 100644 index 9b404c2ca..000000000 --- a/server/src/permissions/group.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -const Rule = require('./rule').Rule; - -class Group { - constructor(row_data) { - this.name = row_data.id; - this.rules = Object.keys(row_data.rules).map((name) => new Rule(name, row_data.rules[name])); - } -} - -module.exports = {Group}; diff --git a/server/src/permissions/rule.js b/server/src/permissions/rule.js deleted file mode 100644 index f00c40ccd..000000000 --- a/server/src/permissions/rule.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; -const Template = require('./template').Template; -const Validator = require('./validator').Validator; - -class Rule { - constructor(name, info) { - this._name = name; - this._template = new Template(info.template); - if (info.validator) { - this._validator = new Validator(info.validator); - } - } - - is_match(query, context) { - return this._template.is_match(query, context); - } - - is_valid() { - if (!this._validator) { - return true; - } - return this._validator.is_valid.apply(this._validator, arguments); - } -} - -class Ruleset { - constructor() { - this.clear(); - } - - clear() { - this._rules = []; - } - - empty() { - return this._rules.length === 0; - } - - update(rules) { - this._rules = rules; - } - - validation_required() { - for (const rule of this._rules) { - if (!rule._validator) { - return false; - } - } - - return true; - } - - // Check that a query passes at least one rule in a set - // Returns the matching rule or undefined if no rules match - // Variadic - extra arguments are passed down to the validator - validate() { - for (const rule of this._rules) { - if (rule.is_valid.apply(rule, arguments)) { - return rule; - } - } - } -} - -// The any_rule is used when permissions are disabled - it allows all queries -const any_rule = new Rule('permissions_disabled', {template: 'any()'}); - -module.exports = {Rule, Ruleset, any_rule}; diff --git a/server/src/permissions/template.js b/server/src/permissions/template.js deleted file mode 100644 index 590007d4e..000000000 --- a/server/src/permissions/template.js +++ /dev/null @@ -1,247 +0,0 @@ -'use strict'; - -const check = require('../error').check; -const remake_error = require('../utils').remake_error; - -const ast = require('@horizon/client/lib/ast'); -const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; -const vm = require('vm'); - -class Any { - constructor(values) { - this._values = values || []; - } - - matches(value, context) { - if (value === undefined) { - return false; - } else if (this._values.length === 0) { - return true; - } - - for (const item of this._values) { - if (template_compare(value, item, context)) { - return true; - } - } - - return false; - } -} - -// This works the same as specifying a literal object in a template, except that -// unspecified key/value pairs are allowed. -class AnyObject { - constructor(obj) { - this._obj = obj || { }; - } - - matches(value, context) { - if (value === null || typeof value !== 'object') { - return false; - } - - for (const key in this._obj) { - if (!template_compare(value[key], this._obj[key], context)) { - return false; - } - } - - return true; - } -} - -// This matches an array where each item matches at least one of the values -// specified at construction. -class AnyArray { - constructor(values) { - this._values = values || []; - } - - matches(value, context) { - if (!Array.isArray(value)) { - return false; - } - - for (const item of value) { - let match = false; - for (const template of this._values) { - if (template_compare(item, template, context)) { - match = true; - break; - } - } - if (!match) { - return false; - } - } - - return true; - } -} - -class UserId { } - -const wrap_write = (query, docs) => { - if (docs instanceof AnyArray || - Array.isArray(docs)) { - query.data = docs; - } else { - query.data = [docs]; - } - return query; -}; - -const wrap_remove = (doc) => { - if (validIndexValue(doc)) { - return {id: doc}; - } - return doc; -}; - -// Add helper methods to match any subset of the current query for reads or writes -ast.TermBase.prototype.anyRead = function() { - return this._sendRequest(new Any(['query', 'subscribe']), - new AnyObject(this._query)); -}; - -ast.Collection.prototype.anyWrite = function() { - let docs = arguments; - if (arguments.length === 0) { - docs = new AnyArray(new Any()); - } - return this._sendRequest(new Any(['store', 'upsert', 'insert', 'replace', 'update', 'remove']), - wrap_write(new AnyObject(this._query), docs)); -}; - -// Monkey-patch the ast functions so we don't clobber certain things -ast.TermBase.prototype.watch = function() { - return this._sendRequest('subscribe', this._query); -}; -ast.TermBase.prototype.fetch = function() { - return this._sendRequest('query', this._query); -}; -ast.Collection.prototype.store = function(docs) { - return this._sendRequest('store', wrap_write(this._query, docs)); -}; -ast.Collection.prototype.upsert = function(docs) { - return this._sendRequest('upsert', wrap_write(this._query, docs)); -}; -ast.Collection.prototype.insert = function(docs) { - return this._sendRequest('insert', wrap_write(this._query, docs)); -}; -ast.Collection.prototype.replace = function(docs) { - return this._sendRequest('replace', wrap_write(this._query, docs)); -}; -ast.Collection.prototype.update = function(docs) { - return this._sendRequest('update', wrap_write(this._query, docs)); -}; -ast.Collection.prototype.remove = function(doc) { - return this._sendRequest('remove', wrap_write(this._query, wrap_remove(doc))); -}; -ast.Collection.prototype.removeAll = function(docs) { - return this._sendRequest('remove', wrap_write(this._query, - docs.map((doc) => wrap_remove(doc)))); -}; - -const env = { - collection: (name) => new ast.Collection((type, options) => - ({request_id: new Any(), - type: Array.isArray(type) ? new Any(type) : type, - options}), name, false), - any: function() { return new Any(Array.from(arguments)); }, - anyObject: function(obj) { return new AnyObject(obj); }, - anyArray: function() { return new AnyArray(Array.from(arguments)); }, - userId: function() { return new UserId(); }, -}; - -const make_template = (str) => { - try { - const sandbox = Object.assign({}, env); - return vm.runInNewContext(str, sandbox); - } catch (err) { - throw remake_error(err); - } -}; - -// eslint-disable-next-line prefer-const -function template_compare(query, template, context) { - if (template === undefined) { - return false; - } else if (template instanceof Any || - template instanceof AnyObject || - template instanceof AnyArray) { - if (!template.matches(query, context)) { - return false; - } - } else if (template instanceof UserId) { - if (query !== context.id) { - return false; - } - } else if (template === null) { - if (query !== null) { - return false; - } - } else if (Array.isArray(template)) { - if (!Array.isArray(query) || - template.length !== query.length) { - return false; - } - for (let i = 0; i < template.length; ++i) { - if (!template_compare(query[i], template[i], context)) { - return false; - } - } - } else if (typeof template === 'object') { - if (typeof query !== 'object') { - return false; - } - - for (const key in query) { - if (!template_compare(query[key], template[key], context)) { - return false; - } - } - - // Make sure all template keys were handled - for (const key in template) { - if (query[key] === undefined) { - return false; - } - } - } else if (template !== query) { - return false; - } - - return true; -} - -const incomplete_template_message = (str) => - `Incomplete template "${str}", ` + - 'consider adding ".fetch()", ".watch()", ".anyRead()", or ".anyWrite()"'; - -class Template { - constructor(str) { - this._value = make_template(str); - check(this._value !== null, `Invalid template: ${str}`); - check(!Array.isArray(this._value), `Invalid template: ${str}`); - check(typeof this._value === 'object', `Invalid template: ${str}`); - if (!(this._value instanceof Any) && !(this._value instanceof AnyObject)) { - if (this._value.request_id === undefined && - this._value.type === undefined && - this._value.options === undefined && - this._value.anyRead) { - this._value = this._value.anyRead(); - } - check(this._value.request_id !== undefined, incomplete_template_message(str)); - check(this._value.type !== undefined, incomplete_template_message(str)); - check(this._value.options !== undefined, incomplete_template_message(str)); - } - } - - is_match(raw_query, context) { - return template_compare(raw_query, this._value, context); - } -} - -module.exports = {Template}; diff --git a/server/src/permissions/validator.js b/server/src/permissions/validator.js deleted file mode 100644 index f3a3cb152..000000000 --- a/server/src/permissions/validator.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -const check = require('../error').check; -const logger = require('../logger'); -const remake_error = require('../utils').remake_error; - -const vm = require('vm'); - -class Validator { - constructor(str) { - try { - this._fn = vm.runInNewContext(str, {}); - } catch (err) { - throw remake_error(err); - } - check(typeof this._fn === 'function'); - } - - is_valid() { - try { - return this._fn.apply(this._fn, arguments); - } catch (err) { - // We don't want to pass the error message on to the user because it might leak - // information about the data. - logger.error(`Exception in validator function: ${err.stack}`); - throw new Error('Validation error'); - } - } -} - - -module.exports = {Validator}; diff --git a/server/src/reql_connection.js b/server/src/reql_connection.js deleted file mode 100644 index 7e1494f08..000000000 --- a/server/src/reql_connection.js +++ /dev/null @@ -1,130 +0,0 @@ -'use strict'; - -const check = require('./error').check; -const logger = require('./logger'); -const Metadata = require('./metadata/metadata').Metadata; -const r = require('rethinkdb'); - -const default_user = 'admin'; -const default_pass = ''; - -class ReqlConnection { - constructor(host, port, db, - auto_create_collection, auto_create_index, - user, pass, connect_timeout, - interruptor) { - this._rdb_options = { - host, - port, - db, - user: user || default_user, - password: pass || default_pass, - timeout: connect_timeout || null, - }; - - this._auto_create_collection = auto_create_collection; - this._auto_create_index = auto_create_index; - this._clients = new Set(); - this._reconnect_delay = 0; - this._retry_timer = null; - - interruptor.catch((err) => { - if (this._retry_timer) { - clearTimeout(this._retry_timer); - } - - this._clients.forEach((client) => - client.close({error: err.message})); - this._clients.clear(); - - this._interrupted_err = err; - this._reconnect(); // This won't actually reconnect, but will do all the cleanup - }); - - logger.info('Connecting to RethinkDB: ' + - `${this._rdb_options.user} @ ${this._rdb_options.host}:${this._rdb_options.port}`); - this._ready_promise = this._reconnect(); - } - - _reconnect() { - if (this._conn) { - this._conn.removeAllListeners('close'); - this._conn.close(); - } - if (this._metadata) { - this._metadata.close(); - } - this._conn = null; - this._metadata = null; - - this._clients.forEach((client) => - client.close({error: 'Connection to the database was lost.'})); - this._clients.clear(); - - if (this._interrupted_err) { - return Promise.reject(this._interrupted_err); - } else if (!this._retry_timer) { - return new Promise((resolve) => { - this._retry_timer = setTimeout(() => resolve(this._init_connection()), this._reconnect_delay); - this._reconnect_delay = Math.min(this._reconnect_delay + 100, 1000); - }); - } - } - - _init_connection() { - this._retry_timer = null; - - return r.connect(this._rdb_options).then((conn) => { - if (this._interrupted_err) { - return Promise.reject(this._interrupted_err); - } - this._conn = conn; - logger.debug('Connection to RethinkDB established.'); - return new Metadata(this._rdb_options.db, - conn, - this._clients, - this._auto_create_collection, - this._auto_create_index).ready(); - }).then((metadata) => { - logger.info('Connection to RethinkDB ready: ' + - `${this._rdb_options.user} @ ${this._rdb_options.host}:${this._rdb_options.port}`); - - this._metadata = metadata; - this._reconnect_delay = 0; - - this._conn.once('close', () => { - logger.error('Lost connection to RethinkDB.'); - this._reconnect(); - }); - - // This is to avoid EPIPE errors - handling is done by the 'close' listener - this._conn.on('error', () => { }); - - return this; - }).catch((err) => { - logger.error(`Connection to RethinkDB terminated: ${err}`); - logger.debug(`stack: ${err.stack}`); - return this._reconnect(); - }); - } - - is_ready() { - return Boolean(this._conn); - } - - ready() { - return this._ready_promise; - } - - connection() { - check(this.is_ready(), 'Connection to the database is down.'); - return this._conn; - } - - metadata() { - check(this.is_ready(), 'Connection to the database is down.'); - return this._metadata; - } -} - -module.exports = {ReqlConnection}; From 8a50370c1fff52b9bc810615196db92c10d9bddc Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 25 Aug 2016 00:32:00 -0700 Subject: [PATCH 34/86] cleanup of obsolete code --- server/src/endpoint/query.js | 15 ++++++++------- server/src/endpoint/writes.js | 6 +++--- server/src/error.js | 3 --- server/src/metadata/index.js | 18 +++++++++--------- server/src/metadata/table.js | 2 +- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/server/src/endpoint/query.js b/server/src/endpoint/query.js index 86c324157..ab3f90b96 100644 --- a/server/src/endpoint/query.js +++ b/server/src/endpoint/query.js @@ -1,9 +1,10 @@ 'use strict'; const query = require('../schema/horizon_protocol').query; -const check = require('../error.js').check; const reql_options = require('./common').reql_options; +const assert = require('assert'); + const Joi = require('joi'); const r = require('rethinkdb'); @@ -34,15 +35,15 @@ const make_reql = (raw_request, metadata) => { if (order_keys.length >= 1) { const k = order_keys[0]; - check(!options.above || options.above[0][k] !== undefined, - '"above" must be on the same field as the first in "order".'); - check(!options.below || options.below[0][k] !== undefined, - '"below" must be on the same field as the first in "order".'); + assert(!options.above || options.above[0][k] !== undefined, + '"above" must be on the same field as the first in "order".'); + assert(!options.below || options.below[0][k] !== undefined, + '"below" must be on the same field as the first in "order".'); } order_keys.forEach((k) => { - check(obj[k] === undefined, - `"${k}" cannot be used in "order", "above", or "below" when finding by that field.`); + assert(obj[k] === undefined, + `"${k}" cannot be used in "order", "above", or "below" when finding by that field.`); }); const index = collection.get_matching_index(fuzzy_fields, order_keys.map((k) => [k])); diff --git a/server/src/endpoint/writes.js b/server/src/endpoint/writes.js index ba1c7902d..8057127b9 100644 --- a/server/src/endpoint/writes.js +++ b/server/src/endpoint/writes.js @@ -1,6 +1,6 @@ 'use strict'; -const check = require('../error').check; +const assert = require('assert'); const r = require('rethinkdb'); @@ -78,7 +78,7 @@ const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, if (ruleset.validation_required()) { // For the set of rows to write, gather info for the validation step return Promise.resolve(pre_validate(row_data.map((data) => data.row))).then((infos) => { - check(infos.length === row_data.length); + assert(infos.length === row_data.length); // For each row to write (and info), validate it with permissions const valid_rows = []; @@ -100,7 +100,7 @@ const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, } return do_write(row_data.map((data) => data.row)).then((res) => res.changes); }).then((changes) => { - check(changes.length === row_data.length); + assert(changes.length === row_data.length); // Remove successful writes and invalidated writes that had an initial version const retry_rows = []; diff --git a/server/src/error.js b/server/src/error.js index c9fa0b7fd..ec81ff3c9 100644 --- a/server/src/error.js +++ b/server/src/error.js @@ -2,8 +2,6 @@ const assert = require('assert'); -const fail = (message) => check(false, message); - class IndexMissing extends Error { constructor(collection, fields) { super(`Collection "${collection.name}" has no index matching ${JSON.stringify(fields)}.`); @@ -35,7 +33,6 @@ class CollectionNotReady extends Error { } module.exports = { - fail, IndexMissing, IndexNotReady, CollectionMissing, diff --git a/server/src/metadata/index.js b/server/src/metadata/index.js index aa9747161..0b5e8f7d0 100644 --- a/server/src/metadata/index.js +++ b/server/src/metadata/index.js @@ -22,7 +22,7 @@ const name_to_info = (name) => { const re = /^hz_(?:(geo)_)?(?:multi_([0-9])+_)?\[/; const matches = name.match(re); - check(matches !== null, `Unexpected index name (invalid format): "${name}"`); + assert(matches !== null, `Unexpected index name (invalid format): "${name}"`); const json_offset = matches[0].length - 1; @@ -32,20 +32,20 @@ const name_to_info = (name) => { try { info.fields = JSON.parse(name.slice(json_offset)); } catch (err) { - check(false, `Unexpected index name (invalid JSON): "${name}"`); + assert(false, `Unexpected index name (invalid JSON): "${name}"`); } // Sanity check fields const validate_field = (f) => { - check(Array.isArray(f), `Unexpected index name (invalid field): "${name}"`); - f.forEach((s) => check(typeof s === 'string', - `Unexpected index name (invalid field): "${name}"`)); + assert(Array.isArray(f), `Unexpected index name (invalid field): "${name}"`); + f.forEach((s) => assert(typeof s === 'string', + `Unexpected index name (invalid field): "${name}"`)); }; - check(Array.isArray(info.fields), - `Unexpected index name (fields are not an array): "${name}"`); - check((info.multi === false) || (info.multi < info.fields.length), - `Unexpected index name (multi index out of bounds): "${name}"`); + assert(Array.isArray(info.fields), + `Unexpected index name (fields are not an array): "${name}"`); + assert((info.multi === false) || (info.multi < info.fields.length), + `Unexpected index name (multi index out of bounds): "${name}"`); info.fields.forEach(validate_field); return info; }; diff --git a/server/src/metadata/table.js b/server/src/metadata/table.js index 82afca07a..9ceead3bf 100644 --- a/server/src/metadata/table.js +++ b/server/src/metadata/table.js @@ -82,7 +82,7 @@ class Table { create_index(fields, conn, done) { const info = {geo: false, multi: false, fields}; const index_name = index.info_to_name(info); - error.check(!this.indexes.get(index_name), 'index already exists'); + assert(!this.indexes.get(index_name), 'index already exists'); const success = () => { // Create the Index object now so we don't try to create it again before the From b3701289d12825d142f8cbadd6d052ee777fab0b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 25 Aug 2016 00:56:15 -0700 Subject: [PATCH 35/86] fixed server --- server/src/client.js | 8 ++++---- server/src/server.js | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/server/src/client.js b/server/src/client.js index cc5a6f208..27755ab30 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -17,7 +17,7 @@ class ClientConnection { this.auth = auth; this.middlewareCb = middlewareCb; this.clientEvents = clientEvents; - this.context = { }; + this._context = { }; this.responses = new Map(); @@ -33,7 +33,7 @@ class ClientConnection { } context() { - return this.context; + return this._context; } handle_websocket_close() { @@ -96,7 +96,7 @@ class ClientConnection { } this.auth.handshake(request).then((res) => { - this.context.user = res.payload; + this._context.user = res.payload; const info = { token: res.token, id: res.payload.id, @@ -105,7 +105,7 @@ class ClientConnection { this.send_message(request, info); this.socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); - this.clientEvents.emit('auth', this.context); + this.clientEvents.emit('auth', this._context); }).catch((err) => { this.close({request_id: request.request_id, error: `${err}`, error_code: 0}); }); diff --git a/server/src/server.js b/server/src/server.js index ee021b25d..909db48d8 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -35,15 +35,15 @@ class Server extends EventEmitter { next(new Error('No middleware to handle the request.')); }; this._middlewareCb = this._defaultMiddlewareCb; - this._auth = new Auth(this, opts.auth); + this._auth = new Auth(this, this.options.auth); this._reliableConn = new ReliableConn({ - host: opts.rdb_host, - port: opts.rdb_port, - db: opts.project_name, - user: opts.rdb_user || 'admin', - password: opts.rdb_password || '', - timeout: opts.rdb_timeout || null, + host: this.options.rdb_host, + port: this.options.rdb_port, + db: this.options.project_name, + user: this.options.rdb_user || 'admin', + password: this.options.rdb_password || '', + timeout: this.options.rdb_timeout || null, }); this._clients = new Set(); @@ -52,11 +52,11 @@ class Server extends EventEmitter { // TODO: consider emitting errors sometimes. this._reliableMetadata = new ReliableMetadata( - opts.project_name, + this.options.project_name, this._reliableConn, this._clients, - opts.auto_create_collection, - opts.auto_create_index); + this.options.auto_create_collection, + this.options.auto_create_index); this._clear_clients_subscription = this._reliableMetadata.subscribe({ onReady: () => { @@ -79,7 +79,7 @@ class Server extends EventEmitter { } }; - const ws_options = {handleProtocols, verifyClient, path: opts.path}; + const ws_options = {handleProtocols, verifyClient, path: this.options.path}; // RSI: only become ready when this and metadata are both ready. const add_websocket = (server) => { From 82d20df8f71ddaa2be82c7768a8c6cca7ef512b2 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 25 Aug 2016 02:30:23 -0700 Subject: [PATCH 36/86] Everything works --- plugin_router/package.json | 1 + plugin_router/src/index.js | 73 ++++++++------------------ plugins/.eslintrc.js | 16 ++++++ plugins/permissions/.babelrc | 3 ++ plugins/permissions/package.json | 13 +++-- plugins/permissions/src/permissions.js | 11 +++- server/src/auth.js | 43 ++++++++------- server/src/client.js | 11 ++-- server/src/reliable.js | 4 ++ test/.eslintrc.js | 17 ++++++ test/serve.js | 61 ++++++++++++--------- 11 files changed, 148 insertions(+), 105 deletions(-) create mode 100644 plugins/.eslintrc.js create mode 100644 plugins/permissions/.babelrc create mode 100644 test/.eslintrc.js diff --git a/plugin_router/package.json b/plugin_router/package.json index e3cc87d8c..cce5efbb3 100644 --- a/plugin_router/package.json +++ b/plugin_router/package.json @@ -21,6 +21,7 @@ "node": ">=4.0.0" }, "dependencies": { + "toposort-class": "^1.0.1" }, "devDependencies": { "eslint": "^2.3.0", diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 1e7b7983c..c1bb50d50 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -1,5 +1,7 @@ 'use strict'; +const Toposort = require('toposort-class'); + class PluginRouter { constructor(server) { this.server = server; @@ -13,13 +15,12 @@ class PluginRouter { return Promise.reject( new Error(`Plugin conflict: "${plugin.name}" already present.`)); } - const activePlugin = Promise.resolve(this.server.ctx()).then(plugin.activate); + const activePlugin = Promise.resolve(this.server).then(plugin.activate); this.plugins[plugin.name] = activePlugin.then((active) => { if (this.httpRoutes[plugin.name]) { throw new Error(`Plugin conflict: "${plugin.name}" already present.`); } - // RSI: validate method name is a legal identifier and doesn't - // conflict with our methods. + for (const m in active.methods) { if (this.methods[m]) { throw new Error(`Method name conflict: "${m}"`); @@ -29,7 +30,7 @@ class PluginRouter { this.httpRoutes[plugin.name] = active; for (const m in active.methods) { this.methods[m] = active.methods[m]; - this._requiresOrdering = null; + this._requirementsOrdering = null; } }); return this.plugins[plugin.name]; @@ -42,7 +43,7 @@ class PluginRouter { return this.plugins[plugin.name].then((active) => { for (const m in active.methods) { delete this.methods[m]; - this._requiresOrdering = null; + this._requirementsOrdering = null; } if (plugin.deactivate) { plugin.deactivate(reason || 'Removed from PluginRouter.'); @@ -50,53 +51,28 @@ class PluginRouter { }); } - requiresOrdering() { - if (!this._requiresOrdering) { - this._requiresOrdering = {}; + requirementsOrdering() { + // RSI: move dependencies and topological sorting into the server + if (!this._requirementsOrdering) { + this._requirementsOrdering = {}; - // RSI: use tsort instead of doing this ourselves like mega chumps. - const graph = {}; + const topo = new Toposort(); for (const m in this.methods) { - if (!graph[m]) { - graph[m] = {name: m, inDegree: 0, children: {}}; - } - if (this.methods[m].requires) { - for (const r in this.methods[m].requires) { - graph[m].inDegree += 1; - if (!graph[r]) { - // RSI: assert that `r` is in `this.methods`. - graph[r] = {name: m, inDegree: 0, children: {}}; + const reqs = this.methods[m].requires; + if (reqs) { + topo.add(m, reqs); + for (const r of reqs) { + if (!this.methods[r]) { + throw new Error( + `Missing method "${r}", which is required by method "${m}".`); } - graph[r].children[m] = true; - } - } - } - - const order = []; - const heap = new Heap((a, b) => a.inDegree - b.inDegree); - for (const g in graph) { - heap.push(graph[g]); - } - while (heap.size() > 0) { - const minItem = heap.pop(); - if (minItem.inDegree !== 0) { - // ERROR: cycle (!!!) - } - for (const c in minItem.children) { - if (graph[c].inDegree <= 0) { - // ERROR: algorithm mistake } - graph[c].inDegree -= 1; - heap.updateItem(graph[c]); } - order.push(minItem.name); } - for (const i in order) { - this._requiresOrdering[order[i]] = i; - } + this._requirementsOrdering = topo.sort().reverse(); } - return this._requiresOrdering; + return this._requirementsOrdering; } httpMiddleware() { @@ -142,7 +118,7 @@ class PluginRouter { } else if (requirements[terminalName]) { next(new Error('terminal ${terminalName} is also a requirement')); } else { - const ordering = this.requiresOrdering(); + const ordering = this.requirementsOrdering(); const middlewareChain = Object.keys(requirements).sort( (a, b) => ordering[a] - ordering[b]); middlewareChain.push(terminalName); @@ -164,10 +140,5 @@ class PluginRouter { } } -function createPluginRouter() { - return new PluginRouter(); -} - -module.exports = createPluginRouter(); -module.exports.PluginRouter = PluginRouter; +module.exports = PluginRouter; diff --git a/plugins/.eslintrc.js b/plugins/.eslintrc.js new file mode 100644 index 000000000..d5f233311 --- /dev/null +++ b/plugins/.eslintrc.js @@ -0,0 +1,16 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "max-len": [ ERROR, 89 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugins/permissions/.babelrc b/plugins/permissions/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/permissions/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index 91cec4c06..dfb1ff365 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -1,11 +1,12 @@ { - "name": "@horizon/plugins/permissions", + "name": "@horizon/plugin-permissions", "version": "1.0.0", "description": "Default permissions plugin for Horizon.", - "main": "src/permissions.js", + "main": "dist/permissions.js", "scripts": { "lint": "eslint src test", - "test": "mocha test" + "test": "mocha test", + "build": "babel src -d dist -s true" }, "repository": { "type": "git", @@ -21,10 +22,14 @@ "node": ">=4.0.0" }, "dependencies": { + "@horizon/client": "2.0.0-beta-5" }, "devDependencies": { + "eslint": "^2.3.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3" + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index d82d0c410..06b78817c 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -290,7 +290,16 @@ class UserCache { // RSI: remove the extra level of function calling. module.exports = (config) => { - const name = config.name || 'permissions'; + config = config || { }; + // RSI: better default handling, use Joi? + if (config.cacheTimeout === undefined) { + config.cacheTimeout = 5000; + } + config.name = config.name || 'permissions'; + config.usersTable = config.usersTable || 'hz_users'; + config.groupsTable = config.groupsTable || 'hz_groups'; + + const name = config.name; const userCache = Symbol(`${name}_userCache`); const userSub = Symbol(`${name}_userSub`); return { diff --git a/server/src/auth.js b/server/src/auth.js index f48793503..af28c1f71 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -98,28 +98,35 @@ class Auth { // TODO: maybe we should write something into the user data to track open sessions/tokens generate(provider, info) { - const key = this.auth_key(provider, info); - const db = r.db(this._parent._reql_conn.metadata()._db); + return Promise.resolve().then(() => { + const conn = this._parent.conn().connection(); + if (!conn) { + throw new Error('No connection with the database.'); + } - const insert = (table, row) => - db.table(table) - .insert(row, {conflict: 'error', returnChanges: 'always'}) - .bracket('changes')(0)('new_val'); + const key = this.auth_key(provider, info); + const db = r.db(this._parent.options.project_name); - let query = db.table('users') - .get(db.table('hz_users_auth').get(key)('user_id')) - .default(r.error('User not found and new user creation is disabled.')); + const insert = (table, row) => + db.table(table) + .insert(row, {conflict: 'error', returnChanges: 'always'}) + .bracket('changes')(0)('new_val'); - if (this._create_new_users) { - query = insert('hz_users_auth', {id: key, user_id: r.uuid()}) - .do((auth_user) => insert('users', this.new_user_row(auth_user('user_id')))); - } + let query = db.table('users') + .get(db.table('hz_users_auth').get(key)('user_id')) + .default(r.error('User not found and new user creation is disabled.')); + + if (this._create_new_users) { + query = insert('hz_users_auth', {id: key, user_id: r.uuid()}) + .do((auth_user) => insert('users', this.new_user_row(auth_user('user_id')))); + } - return query.run(this._parent._reql_conn.connection()).catch((err) => { - // TODO: if we got a `Duplicate primary key` error, it was likely a race condition - // and we should succeed if we try again. - logger.debug(`Failed user lookup or creation: ${err}`); - throw new Error('User lookup or creation in database failed.'); + return query.run(conn).catch((err) => { + // TODO: if we got a `Duplicate primary key` error, it was likely a race condition + // and we should succeed if we try again. + logger.debug(`Failed user lookup or creation: ${err}`); + throw new Error('User lookup or creation in database failed.'); + }); }).then((user) => this._jwt.sign({id: user.id, provider})); } diff --git a/server/src/client.js b/server/src/client.js index 27755ab30..02bae3bcb 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -38,7 +38,7 @@ class ClientConnection { handle_websocket_close() { logger.debug('ClientConnection closed.'); - this.responses.forEach((response) => response.close()); + this.responses.forEach((response) => response.end()); this.responses.clear(); } @@ -126,16 +126,17 @@ class ClientConnection { const response = new Response((obj) => this.send_message(raw_request, obj)); this.responses.set(raw_request.request_id, response); - this.middlewareCb(raw_request, response); + response.complete.then(() => this.remove_request(raw_request.request_id)); + + this.middlewareCb(raw_request, response, (err) => + response.end(err || new Error(`Request ran past the end of the middleware stack.`))); } remove_response(request_id) { const response = this.responses.get(request_id); this.responses.delete(request_id); - if (response) { - response.close(); - } + response.end(); } is_open() { diff --git a/server/src/reliable.js b/server/src/reliable.js index 21714db61..5227d26d3 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -111,6 +111,10 @@ class ReliableConn extends Reliable { }); } + connection() { + return this.ready ? this.conn : null; + } + close(reason) { let retProm = super.close(reason); if (this.conn) { diff --git a/test/.eslintrc.js b/test/.eslintrc.js new file mode 100644 index 000000000..bc66210fa --- /dev/null +++ b/test/.eslintrc.js @@ -0,0 +1,17 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "camelcase": [ ERROR ], + "max-len": [ ERROR, 89 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/test/serve.js b/test/serve.js index a141bc013..a0f0e8ff3 100755 --- a/test/serve.js +++ b/test/serve.js @@ -6,6 +6,8 @@ require('../server/node_modules/source-map-support').install(); Error.stackTraceLimit = Infinity; const horizon = require('../server'); +const PluginRouter = require('../plugin_router'); +const permissions = require('../plugins/permissions'); // Utilities provided by the CLI library const each_line_in_pipe = require('../cli/src/utils/each_line_in_pipe'); @@ -32,25 +34,25 @@ const test_dist_dir = path.resolve(__dirname, '../client/dist'); const examples_dir = path.resolve(__dirname, '../examples'); const parser = new argparse.ArgumentParser(); -parser.addArgument([ '--port', '-p' ], - { type: 'int', defaultValue: 8181, metavar: 'PORT', - help: 'Local port to serve HTTP assets and horizon on.' }); +parser.addArgument(['--port', '-p'], + {type: 'int', defaultValue: 8181, metavar: 'PORT', + help: 'Local port to serve HTTP assets and horizon on.'}); -parser.addArgument([ '--bind', '-b' ], - { type: 'string', defaultValue: [ 'localhost' ], action: 'append', metavar: 'HOST', - help: 'Local hostname(s) to serve HTTP and horizon on (repeatable).' }); +parser.addArgument(['--bind', '-b'], + {type: 'string', defaultValue: ['localhost'], action: 'append', metavar: 'HOST', + help: 'Local hostname(s) to serve HTTP and horizon on (repeatable).'}); -parser.addArgument([ '--keep', '-k' ], - { defaultValue: false, action: 'storeTrue', - help: 'Keep the existing "rethinkdb_data_test" directory.' }); +parser.addArgument(['--keep', '-k'], + {defaultValue: false, action: 'storeTrue', + help: 'Keep the existing "rethinkdb_data_test" directory.'}); -parser.addArgument([ '--permissions' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', defaultValue: 'no', - help: 'Enable or disable checking permissions on requests, defaults to disabled.' }); +parser.addArgument(['--permissions'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', defaultValue: 'no', + help: 'Enable or disable checking permissions on requests, defaults to disabled.'}); const options = parser.parseArgs(); -if (options.bind.indexOf('all') !== -1) { options.bind = [ '0.0.0.0' ]; } +if (options.bind.indexOf('all') !== -1) { options.bind = ['0.0.0.0']; } process.on('SIGINT', () => process.exit(0)); process.on('SIGTERM', () => process.exit(0)); @@ -58,20 +60,20 @@ process.on('SIGTERM', () => process.exit(0)); const serve_file = (file_path, res) => { fs.access(file_path, fs.R_OK | fs.F_OK, (exists) => { if (exists) { - res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.writeHead(404, {'Content-Type': 'text/plain'}); res.end(`File "${file_path}" not found\n`); } else { fs.readFile(file_path, 'binary', (err, file) => { if (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.writeHead(500, {'Content-Type': 'text/plain'}); res.end(`${err}\n`); } else { if (file_path.endsWith('.js')) { - res.writeHead(200, { 'Content-Type': 'application/javascript' }); + res.writeHead(200, {'Content-Type': 'application/javascript'}); } else if (file_path.endsWith('.html')) { - res.writeHead(200, { 'Content-Type': 'text/html' }); + res.writeHead(200, {'Content-Type': 'text/html'}); } else { - res.writeHead(200); + res.writeHead(200); } res.end(file, 'binary'); } @@ -81,11 +83,11 @@ const serve_file = (file_path, res) => { }; // On Windows, `npm` is actually `npm.cmd` -const npm_cmd = process.platform === "win32" ? "npm.cmd" : "npm"; +const npm_cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; // Run the client build -const build_proc = child_process.spawn(npm_cmd, [ 'run', 'dev'], - { cwd: test_dist_dir }); +const build_proc = child_process.spawn(npm_cmd, ['run', 'dev'], + {cwd: test_dist_dir}); build_proc.on('exit', () => process.exit(1)); process.on('exit', () => build_proc.kill('SIGTERM')); @@ -122,13 +124,15 @@ const http_servers = options.bind.map((host) => new http.Server((req, res) => { const req_path = url.parse(req.url).pathname; if (req_path.indexOf('/examples/') === 0) { - serve_file(path.resolve(examples_dir, req_path.replace(/^[/]examples[/]/, '')), res); + serve_file(path.resolve(examples_dir, + req_path.replace(/^[/]examples[/]/, '')), res); } else { if (!client_ready) { - res.writeHead(503, { 'Content-Type': 'text/plain' }); + res.writeHead(503, {'Content-Type': 'text/plain'}); res.end('Initial client build is ongoing, try again in a few seconds.'); } else { - serve_file(path.resolve(test_dist_dir, req_path.replace(/^[/]/, '')), res); + serve_file(path.resolve(test_dist_dir, + req_path.replace(/^[/]/, '')), res); } } })); @@ -148,7 +152,7 @@ new Promise((resolve) => { }; options.bind.forEach((host) => { - dns.lookup(host, { all: true }, add_results); + dns.lookup(host, {all: true}, add_results); }); }).then((local_addresses) => { // Launch rethinkdb - once we know the port we can attach horizon to the http server @@ -158,7 +162,7 @@ new Promise((resolve) => { console.log('starting rethinkdb'); - return start_rdb_server({ bind: local_addresses, dataDir: data_dir }); + return start_rdb_server({bind: local_addresses, dataDir: data_dir}); }).then((server) => { assert.notStrictEqual(server.driver_port, undefined); console.log(`RethinkDB server listening for clients on port ${server.driver_port}.`); @@ -180,6 +184,11 @@ new Promise((resolve) => { }); console.log('starting http servers'); + const plugins = new PluginRouter(horizon_server); + plugins.add(permissions()); + + horizon_server.set_middleware(plugins.hzMiddleware()); + // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server http_servers.forEach((serv, i) => { const extant_listeners = serv.listeners('request').slice(0); From c62e3e04ef74469e2ce08679dc20cb97a18a467b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 26 Aug 2016 16:49:19 -0700 Subject: [PATCH 37/86] everything works a little more --- .gitignore | 7 +++- plugin_router/src/index.js | 4 ++ plugins/permissions/package.json | 1 - plugins/permissions/src/permissions.js | 54 +++++++++++++++++--------- plugins/permissions/src/rule.js | 3 +- server/.gitignore | 6 --- server/src/server.js | 3 +- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index d8a67a7ef..af18e5e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -client/dist/ client/lib/ rethinkdb_data_test rethinkdb_data/ @@ -11,3 +10,9 @@ node_modules/ .hz/ config.toml **/.vscode + +**/dist/ + +# RethinkDB stuff +**/rethinkdb_data/ +**/rethinkdb_data_test/ diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index c1bb50d50..888802489 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -32,6 +32,10 @@ class PluginRouter { this.methods[m] = active.methods[m]; this._requirementsOrdering = null; } + }).catch((err) => { + this.server.logger.error(`Error when adding plugin ${plugin.name}: ${err}`); + this.server.logger.debug(`${err.stack}`); + throw err; }); return this.plugins[plugin.name]; } diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index dfb1ff365..f315a160a 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -25,7 +25,6 @@ "@horizon/client": "2.0.0-beta-5" }, "devDependencies": { - "eslint": "^2.3.0", "istanbul": "^0.4.3", "mocha": "^2.3.3", diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 06b78817c..ab030776a 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -133,7 +133,7 @@ class UserCache { let oldGroups = new Set(); const cfeed = new ctx.ReliableChangefeed( r.table(config.usersTable).get(userId).changes({includeInitial: true}), - ctx.reliableConn, + ctx.rdb_connection(), { onUnready: () => { cfeed.unreadyAt = new Date(); @@ -166,14 +166,17 @@ class UserCache { this.groupsUnreadyAt = new Date(0); // epoch this.groupCfeed = new ctx.ReliableChangefeed( r.table(config.groupsTable).changes({includeInitial: true}), - ctx.reliableConn, + ctx.rdb_connection(), { onReady: () => { this.ruleMap.delAllGroupRules(); this.queuedGroups.forEach( - (rules, groupId) => rules.forEach( - (rule) => this.ruleMap.addGroupRule( - groupId, JSON.stringify([groupId, rule.name]), new Rule(rule)))); + (rules, groupId) => { + for (const name in rules) { + const ruleId = JSON.stringify([groupId, name]); + this.ruleMap.addGroupRule(groupId, ruleId, new Rule(rules[name])); + } + }); this.queuedGroups.clear(); assert(this.groupsUnreadyAt !== null); @@ -187,7 +190,11 @@ class UserCache { onChange: (change) => { const id = change.old_val ? change.old_val.id : change.new_val.id; if (this.groupsUnreadyAt !== null) { - this.queuedGroups.set(id, change.rules); + if (change.new_val) { + this.queuedGroups.set(id, change.new_val.rules); + } else { + this.queuedGroups.delete(id); + } } else { const oldRules = change.old_val ? change.old_val.rules : {}; const newRules = change.new_val ? change.new_val.rules : {}; @@ -289,9 +296,9 @@ class UserCache { } // RSI: remove the extra level of function calling. -module.exports = (config) => { - config = config || { }; +module.exports = (raw_config) => { // RSI: better default handling, use Joi? + const config = raw_config || { }; if (config.cacheTimeout === undefined) { config.cacheTimeout = 5000; } @@ -302,12 +309,27 @@ module.exports = (config) => { const name = config.name; const userCache = Symbol(`${name}_userCache`); const userSub = Symbol(`${name}_userSub`); + let authCb; + let disconnectCb; return { name, activate(ctx) { ctx.logger.info('Activating plugins module.'); ctx[userCache] = new UserCache(config, ctx); + + authCb = (clientCtx) => { + clientCtx[userSub] = ctx[userCache].subscribe(clientCtx.user.id); + }; + ctx.on('auth', authCb); + + disconnectCb = (clientCtx) => { + if (clientCtx[userSub]) { + clientCtx[userSub].close(); + } + }; + ctx.on('disconnect', disconnectCb); + return { methods: { hz_permissions: { @@ -324,20 +346,16 @@ module.exports = (config) => { }, }, }, - onClientEvent: { - auth: (clientCtx) => { - clientCtx[userSub] = ctx[userCache].subscribe(clientCtx.user.id); - }, - disconnect: (clientCtx) => { - if (clientCtx[userSub]) { - clientCtx[userSub].close(); - } - }, - }, }; }, deactivate(ctx) { + if (authCb) { + ctx.removeListener('auth', authCb); + } + if (disconnectCb) { + ctx.removeListener('disconnect', disconnectCb); + } if (ctx[userCache]) { ctx[userCache].close(); } diff --git a/plugins/permissions/src/rule.js b/plugins/permissions/src/rule.js index e82755bcb..608c6bc5e 100644 --- a/plugins/permissions/src/rule.js +++ b/plugins/permissions/src/rule.js @@ -3,8 +3,7 @@ const Template = require('./template').Template; const Validator = require('./validator').Validator; class Rule { - constructor(name, info) { - this.name = name; + constructor(info) { this.template = new Template(info.template); if (info.validator) { this.validator = new Validator(info.validator); diff --git a/server/.gitignore b/server/.gitignore index 72d916d0d..c7acb5d90 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,11 +1,5 @@ -cert.pem -key.pem node_modules *.log # Coverage directory used by tools like istanbul coverage - -# RethinkDB stuff -rethinkdb_data/ -rethinkdb_data_test/ \ No newline at end of file diff --git a/server/src/server.js b/server/src/server.js index 909db48d8..313f4c8c6 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -49,6 +49,7 @@ class Server extends EventEmitter { this.r = r; this.logger = logger; + this.ReliableChangefeed = ReliableChangefeed; // RSI: better place to put this that on the context? Should plugins require the server? // TODO: consider emitting errors sometimes. this._reliableMetadata = new ReliableMetadata( @@ -129,7 +130,7 @@ class Server extends EventEmitter { return this._reliableMetadata; } - conn() { + rdb_connection() { return this._reliableConn; } From 386a8e34c8359e85d5e4f1a333787645b0b569d1 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 30 Aug 2016 15:38:33 -0700 Subject: [PATCH 38/86] getting stuff running --- plugin_router/src/index.js | 8 +-- plugins/permissions/src/permissions.js | 9 ++-- server/src/auth.js | 4 +- server/src/client.js | 57 ++++++++++++--------- server/src/reliable.js | 3 +- server/src/schema/horizon_protocol.js | 68 ++++---------------------- test/serve.js | 14 ++++-- 7 files changed, 67 insertions(+), 96 deletions(-) diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 888802489..f7c6e455a 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -110,8 +110,10 @@ class PluginRouter { } else { requirements[o] = true; } - for (const r of m.requires) { - requirements[r] = true; + if (m.requires) { + for (const r of m.requires) { + requirements[r] = true; + } } } } @@ -133,7 +135,7 @@ class PluginRouter { next(maybeErr); } else { try { - this.methods[methodName].impl(req, res, cb); + this.methods[methodName].handler(new Request(req, methodName), res, cb); } catch (e) { next(e); } diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index ab030776a..ce8aee847 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -219,7 +219,7 @@ class UserCache { subscribe(userId) { let cfeed = this.userCfeeds.get(userId); if (!cfeed) { - this.userCfeeds.set(userId, cfeed = this.newUserCfeed()); + this.userCfeeds.set(userId, cfeed = this.newUserCfeed(userId)); cfeed.readyPromise = new Promise((resolve, reject) => { cfeed.subscribe({onReady: () => resolve()}); setTimeout(() => reject(new Error('timed out')), this.timeout); @@ -268,6 +268,8 @@ class UserCache { if (!needsValidation) { return null; } + + // The validator function returns the matching rule if allowed, or undefined return (...args) => { try { for (const rule of ruleset) { @@ -279,7 +281,6 @@ class UserCache { // We don't want to pass the error message on to the user because // it might leak information about the data. this.ctx.logger.error(`Exception in validator function: ${err.stack}`); - throw new Error('Validation error'); } }; }; @@ -303,7 +304,7 @@ module.exports = (raw_config) => { config.cacheTimeout = 5000; } config.name = config.name || 'permissions'; - config.usersTable = config.usersTable || 'hz_users'; + config.usersTable = config.usersTable || 'users'; config.groupsTable = config.groupsTable || 'hz_groups'; const name = config.name; @@ -315,7 +316,7 @@ module.exports = (raw_config) => { name, activate(ctx) { - ctx.logger.info('Activating plugins module.'); + ctx.logger.info('Activating permissions plugin.'); ctx[userCache] = new UserCache(config, ctx); authCb = (clientCtx) => { diff --git a/server/src/auth.js b/server/src/auth.js index af28c1f71..98c986f1d 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -2,7 +2,6 @@ const logger = require('./logger'); const options_schema = require('./schema/server_options').auth; -const writes = require('./endpoint/writes'); const Joi = require('joi'); const Promise = require('bluebird'); @@ -92,14 +91,13 @@ class Auth { return { id, groups: ['default', this._new_user_group], - [writes.version_field]: 0, }; } // TODO: maybe we should write something into the user data to track open sessions/tokens generate(provider, info) { return Promise.resolve().then(() => { - const conn = this._parent.conn().connection(); + const conn = this._parent.rdb_connection().connection(); if (!conn) { throw new Error('No connection with the database.'); } diff --git a/server/src/client.js b/server/src/client.js index 02bae3bcb..865574dd3 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -2,6 +2,7 @@ const logger = require('./logger'); const schemas = require('./schema/horizon_protocol'); +const Request = require('./request'); const Response = require('./response'); const Joi = require('joi'); @@ -76,13 +77,13 @@ class ClientConnection { } catch (err) { const detail = err.details[0]; const err_str = `Request validation error at "${detail.path}": ${detail.message}`; - const request_id = request.request_id === undefined ? null : request.request_id; + const reqId = request.request_id === undefined ? null : request.request_id; if (request.request_id === undefined) { // This is pretty much an unrecoverable protocol error, so close the connection - this.close({request_id, error: `Protocol error: ${err}`, error_code: 0}); + this.close({reqId, error: `Protocol error: ${err}`, error_code: 0}); } else { - this.send_error({request_id}, err_str); + this.send_error(reqId, new Error(err_str)); } } } @@ -95,19 +96,20 @@ class ClientConnection { return this.close({error: 'Invalid handshake.', error_code: 0}); } + const reqId = request.request_id; this.auth.handshake(request).then((res) => { this._context.user = res.payload; - const info = { + this.send_message(reqId, { token: res.token, id: res.payload.id, provider: res.payload.provider, - }; - this.send_message(request, info); + }); this.socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); this.clientEvents.emit('auth', this._context); }).catch((err) => { - this.close({request_id: request.request_id, error: `${err}`, error_code: 0}); + logger.debug(`Error during client handshake: ${err.stack}`); + this.close({request_id: reqId, error: `${err}`, error_code: 0}); }); } @@ -117,16 +119,25 @@ class ClientConnection { if (raw_request === undefined) { return; + } + + const reqId = raw_request.request_id; + if (raw_request.type === 'keepalive') { + return this.send_message(reqId, {state: 'complete'}); } else if (raw_request.type === 'end_subscription') { // there is no response for end_subscription - return this.remove_response(raw_request.request_id); - } else if (raw_request.type === 'keepalive') { - return this.send_message(raw_request, {state: 'complete'}); + return this.remove_response(reqId); + } else if (this.responses.get(reqId)) { + return this.close({ error: `Received duplicate request_id: ${reqId}` }); } - const response = new Response((obj) => this.send_message(raw_request, obj)); - this.responses.set(raw_request.request_id, response); - response.complete.then(() => this.remove_request(raw_request.request_id)); + Object.freeze(raw_request.options); + raw_request.clientCtx = this._context; + raw_request._parameters = {}; + + const response = new Response((obj) => this.send_message(reqId, obj)); + this.responses.set(reqId, response); + response.complete.then(() => this.remove_request(reqId)); this.middlewareCb(raw_request, response, (err) => response.end(err || new Error(`Request ran past the end of the middleware stack.`))); @@ -145,34 +156,34 @@ class ClientConnection { close(info) { if (this.is_open()) { - const close_msg = + const reason = (info.error && info.error.substr(0, 64)) || 'Unspecified reason.'; - logger.debug('Closing ClientConnection with message: ' + - `${info.error || 'Unspecified reason.'}`); - logger.debug(`info: ${JSON.stringify(info)}`); + logger.debug('Closing ClientConnection with reason: ' + + `${reason}`); + logger.debug(`Final message: ${JSON.stringify(info)}`); if (info.request_id !== undefined) { this.socket.send(JSON.stringify(info)); } - this.socket.close(1002, close_msg); + this.socket.close(1002, reason); } } - send_message(request, data) { + send_message(reqId, data) { // Ignore responses for disconnected clients if (this.is_open()) { - data.request_id = request.request_id; + data.request_id = reqId; logger.debug(`Sending response: ${JSON.stringify(data)}`); this.socket.send(JSON.stringify(data)); } } - send_error(request, err, code) { + send_error(reqId, err, code) { logger.debug( - `Sending error result for request ${request.request_id}:\n${err.stack}`); + `Sending error result for request ${reqId}:\n${err.stack}`); const error = err instanceof Error ? err.message : err; const error_code = code === undefined ? -1 : code; - this.send_message(request, {error, error_code}); + this.send_message(reqId, {error, error_code}); } } diff --git a/server/src/reliable.js b/server/src/reliable.js index 5227d26d3..c2afd157a 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -155,9 +155,10 @@ class ReliableChangefeed extends Reliable { } }).then((res) => { // If we get here the cursor closed for some reason. - throw new Error(`cursor closed for some reason: ${res}`); + throw new Error(`cursor closed unexpectedly: ${res}`); }); }).catch((e) => { + logger.debug(`Changefeed error (${this.reql}): ${e}`); if (this.ready) { this.emit('onUnready', e); } diff --git a/server/src/schema/horizon_protocol.js b/server/src/schema/horizon_protocol.js index 851f983e3..17943c481 100644 --- a/server/src/schema/horizon_protocol.js +++ b/server/src/schema/horizon_protocol.js @@ -8,70 +8,20 @@ const handshake = Joi.object().keys({ token: Joi.string().required() .when('method', {is: Joi.not('token').required(), then: Joi.forbidden()}), }).unknown(false); - -const read = Joi.alternatives().try( - Joi.object().keys({ - collection: Joi.string().token().required(), - find: Joi.object().min(1).unknown(true).required(), - }).unknown(false), - Joi.object().keys({ - collection: Joi.string().token().required(), - - limit: Joi.number().integer().greater(-1).optional() - .when('find', {is: Joi.any().required(), then: Joi.forbidden()}), - - order: Joi.array().ordered( - Joi.array().items(Joi.string()).min(1).unique().label('fields').required(), - Joi.string().valid('ascending', 'descending').label('direction').required()).optional() - .when('find_all', {is: Joi.array().min(2).required(), then: Joi.forbidden()}), - - above: Joi.array().ordered( - Joi.object().length(1).unknown(true).label('value').required(), - Joi.string().valid('open', 'closed').label('bound_type').required()).optional() - .when('find_all', {is: Joi.array().min(2).required(), then: Joi.forbidden()}), - - below: Joi.array().ordered( - Joi.object().length(1).unknown(true).label('value').required(), - Joi.string().valid('open', 'closed').label('bound_type').required()).optional() - .when('find_all', {is: Joi.array().min(2).required(), then: Joi.forbidden()}), - - find_all: Joi.array().items(Joi.object().min(1).label('item').unknown(true)).min(1).optional(), - }).unknown(false) -); - -const write_id_optional = Joi.object({ - timeout: Joi.number().integer().greater(-1).optional().default(null), - collection: Joi.string().token().required(), - data: Joi.array().min(1).items(Joi.object({ - id: Joi.any().optional(), - }).unknown(true)).required(), -}).unknown(false); - -const write_id_required = Joi.object({ - timeout: Joi.number().integer().greater(-1).optional().default(null), - collection: Joi.string().token().required(), - data: Joi.array().min(1).items(Joi.object({ - id: Joi.any().required(), - }).unknown(true)).required(), -}).unknown(false); - +// RSI: get this working again +// const request = Joi.object({ +// request_id: Joi.number().required(), +// type: Joi.only('end_subscription', 'keepalive').optional(), +// options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required() +// .when('type', {is: Joi.string().only('end_subscription', 'keepalive'), then: Joi.forbidden()}) +// }).unknown(false); +// const request = Joi.object({ request_id: Joi.number().required(), - type: Joi.string().required(), - options: Joi.object().required() - .when('type', {is: Joi.string().only('end_subscription'), then: Joi.forbidden()}) - .when('type', {is: Joi.string().only('keepalive'), then: Joi.forbidden()}), + options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required(), }).unknown(false); module.exports = { handshake, request, - query: read, - subscribe: read, - insert: write_id_optional, - store: write_id_optional, - upsert: write_id_optional, - update: write_id_required, - replace: write_id_required, - remove: write_id_required, }; diff --git a/test/serve.js b/test/serve.js index a0f0e8ff3..0cf6e98a0 100755 --- a/test/serve.js +++ b/test/serve.js @@ -7,7 +7,11 @@ Error.stackTraceLimit = Infinity; const horizon = require('../server'); const PluginRouter = require('../plugin_router'); -const permissions = require('../plugins/permissions'); +const permit_all = require('../plugins/permit_all'); +const reads = require('../plugins/reads'); +const writes = require('../plugins/writes'); +const collection = require('../plugins/collection'); +const timeout = require('../plugins/timeout'); // Utilities provided by the CLI library const each_line_in_pipe = require('../cli/src/utils/each_line_in_pipe'); @@ -175,7 +179,7 @@ new Promise((resolve) => { auto_create_index: true, rdb_port: server.driver_port, permissions: parse_yes_no_option(options.permissions), - project_name: 'test', + project_name: 'hz_test', auth: { allow_unauthenticated: true, allow_anonymous: true, @@ -185,7 +189,11 @@ new Promise((resolve) => { console.log('starting http servers'); const plugins = new PluginRouter(horizon_server); - plugins.add(permissions()); + plugins.add(permit_all()); + plugins.add(collection()); + plugins.add(timeout()); + plugins.add(reads()); + plugins.add(writes()); horizon_server.set_middleware(plugins.hzMiddleware()); From 75cc00a8e2f331aa4fa32dfa9b72c9596a8b561b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 30 Aug 2016 17:59:37 -0700 Subject: [PATCH 39/86] plugins almost working --- plugin_router/src/index.js | 6 + plugin_router/src/request.js | 22 ++ plugins/collection/package.json | 33 +++ .../collection/src}/collection.js | 2 +- .../collection/src}/index.js | 0 plugins/collection/src/main.js | 31 +++ .../collection/src}/reliable_metadata.js | 85 ++++---- .../collection/src}/table.js | 0 plugins/permit_all/.babelrc | 3 + plugins/permit_all/package.json | 33 +++ plugins/permit_all/src/index.js | 17 ++ plugins/reads/.babelrc | 3 + plugins/reads/package.json | 33 +++ plugins/reads/src/above.js | 17 ++ plugins/reads/src/below.js | 17 ++ plugins/reads/src/common.js | 131 ++++++++++++ plugins/reads/src/fetch.js | 52 +++++ plugins/reads/src/find.js | 15 ++ plugins/reads/src/findAll.js | 15 ++ plugins/reads/src/index.js | 52 +++++ plugins/reads/src/limit.js | 13 ++ plugins/reads/src/order.js | 31 +++ plugins/reads/src/watch.js | 48 +++++ plugins/timeout/package.json | 33 +++ plugins/timeout/src/index.js | 23 ++ plugins/writes/.babelrc | 3 + plugins/writes/package.json | 33 +++ plugins/writes/src/common.js | 198 ++++++++++++++++++ plugins/writes/src/index.js | 46 ++++ plugins/writes/src/insert.js | 32 +++ plugins/writes/src/remove.js | 54 +++++ plugins/writes/src/replace.js | 44 ++++ plugins/writes/src/store.js | 58 +++++ plugins/writes/src/update.js | 49 +++++ plugins/writes/src/upsert.js | 64 ++++++ protocol.md | 2 +- server/src/client.js | 1 - server/src/response.js | 3 + 38 files changed, 1252 insertions(+), 50 deletions(-) create mode 100644 plugin_router/src/request.js create mode 100644 plugins/collection/package.json rename {server/src/metadata => plugins/collection/src}/collection.js (99%) rename {server/src/metadata => plugins/collection/src}/index.js (100%) create mode 100644 plugins/collection/src/main.js rename {server/src/metadata => plugins/collection/src}/reliable_metadata.js (82%) rename {server/src/metadata => plugins/collection/src}/table.js (100%) create mode 100644 plugins/permit_all/.babelrc create mode 100644 plugins/permit_all/package.json create mode 100644 plugins/permit_all/src/index.js create mode 100644 plugins/reads/.babelrc create mode 100644 plugins/reads/package.json create mode 100644 plugins/reads/src/above.js create mode 100644 plugins/reads/src/below.js create mode 100644 plugins/reads/src/common.js create mode 100644 plugins/reads/src/fetch.js create mode 100644 plugins/reads/src/find.js create mode 100644 plugins/reads/src/findAll.js create mode 100644 plugins/reads/src/index.js create mode 100644 plugins/reads/src/limit.js create mode 100644 plugins/reads/src/order.js create mode 100644 plugins/reads/src/watch.js create mode 100644 plugins/timeout/package.json create mode 100644 plugins/timeout/src/index.js create mode 100644 plugins/writes/.babelrc create mode 100644 plugins/writes/package.json create mode 100644 plugins/writes/src/common.js create mode 100644 plugins/writes/src/index.js create mode 100644 plugins/writes/src/insert.js create mode 100644 plugins/writes/src/remove.js create mode 100644 plugins/writes/src/replace.js create mode 100644 plugins/writes/src/store.js create mode 100644 plugins/writes/src/update.js create mode 100644 plugins/writes/src/upsert.js diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index f7c6e455a..cf0d3dc00 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -1,5 +1,7 @@ 'use strict'; +const Request = require('./request'); + const Toposort = require('toposort-class'); class PluginRouter { @@ -22,6 +24,7 @@ class PluginRouter { } for (const m in active.methods) { + console.log(`adding plugin method: ${m}`); if (this.methods[m]) { throw new Error(`Method name conflict: "${m}"`); } @@ -100,6 +103,7 @@ class PluginRouter { for (const o in req.options) { const m = this.methods[o]; if (m) { + console.log(`method for request: ${o}, type: ${m.type}`); if (m.type === 'terminal') { if (terminalName !== null) { next(new Error('multiple terminals in request: ' + @@ -115,6 +119,8 @@ class PluginRouter { requirements[r] = true; } } + } else { + console.log(`no ${o} method for request`); } } } diff --git a/plugin_router/src/request.js b/plugin_router/src/request.js new file mode 100644 index 000000000..bf7f075c4 --- /dev/null +++ b/plugin_router/src/request.js @@ -0,0 +1,22 @@ +'use strict'; + +class Request { + constructor(request, currentMethod) { + this.request_id = request.request_id; + this.options = request.options; + this.clientCtx = request.clientCtx; + this._parameters = request.parameters; + this._currentMethod = currentMethod; + } + + getParameter(methodName) { + return this._parameters[methodName]; + } + + setParameter(value) { + assert(this._currentMethod); + return this._parameters[this._currentMethod] = value; + } +} + +module.exports = Request; diff --git a/plugins/collection/package.json b/plugins/collection/package.json new file mode 100644 index 000000000..7d26ea8eb --- /dev/null +++ b/plugins/collection/package.json @@ -0,0 +1,33 @@ +{ + "name": "@horizon/plugin-collection", + "version": "1.0.0", + "description": "Plugin that gets the collection for Horizon operations.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/server/src/metadata/collection.js b/plugins/collection/src/collection.js similarity index 99% rename from server/src/metadata/collection.js rename to plugins/collection/src/collection.js index faca1335c..7ec5500e4 100644 --- a/server/src/metadata/collection.js +++ b/plugins/collection/src/collection.js @@ -77,7 +77,7 @@ class Collection { get_matching_index(fuzzy_fields, ordered_fields) { const match = this._get_table().get_matching_index(fuzzy_fields, ordered_fields); - + if (match && !match.ready()) { throw new error.IndexNotReady(this, match); } else if (!match) { diff --git a/server/src/metadata/index.js b/plugins/collection/src/index.js similarity index 100% rename from server/src/metadata/index.js rename to plugins/collection/src/index.js diff --git a/plugins/collection/src/main.js b/plugins/collection/src/main.js new file mode 100644 index 000000000..7415f73c4 --- /dev/null +++ b/plugins/collection/src/main.js @@ -0,0 +1,31 @@ +'use strict'; + +const collection = (server, metadata) => (req, res, next) => { + const args = req.options.collection; + if (args.length !== 1) { + next(new Error(`"collection" expected 1 argument but found ${args.length}.`)); + } else if (typeof args[0] !== 'string') { + next(new Error('First argument to "collection" must be a string.')); + } else { + metadata.collection(args[0]).then((collection) => { + req.setParameter(collection); + next(); + }).catch(next); + } +}; + +module.exports = (raw_config) => ({ + name: (raw_config && raw_config.name) || 'permissions', + activate: (server) => { + const metadata = new ReliableMetadata(...); + + return { + methods: { + collection: { + type: 'option', + handler: collection(server, metadata), + }, + }, + }; + }, +}); diff --git a/server/src/metadata/reliable_metadata.js b/plugins/collection/src/reliable_metadata.js similarity index 82% rename from server/src/metadata/reliable_metadata.js rename to plugins/collection/src/reliable_metadata.js index 329aeeefc..ace56856c 100644 --- a/server/src/metadata/reliable_metadata.js +++ b/plugins/collection/src/reliable_metadata.js @@ -306,69 +306,60 @@ class ReliableMetadata extends Reliable { ]); } - // Public interface for use by plugins or other classes + // Public interface for use by plugins or other classes, + // returns a Promise of a collection object collection(name) { - if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { - throw new Error(`Collection "${name}" is reserved for internal use ` + - 'and cannot be used in requests.'); - } else if (!this.ready) { - throw new Error('ReliableMetadata is not ready.'); - } + return Promise.resolve().then(() => { + if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { + throw new Error(`Collection "${name}" is reserved for internal use ` + + 'and cannot be used in requests.'); + } else if (!this.ready) { + throw new Error('ReliableMetadata is not ready.'); + } - const collection = this._collections.get(name); - if (!collection) { throw new error.CollectionMissing(name); } - if (!collection._get_table().ready()) { throw new error.CollectionNotReady(collection); } - return collection; + const collection = this._collections.get(name); + if (!collection && !this._auto_create_collection) { + throw new Error(`Collection "${name}" does not exist.`); + } + return collection || this.create_collection(name); + }); } - create_collection(name, done) { - if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { - throw new Error(`Collection "${name}" is reserved for internal use ` + - 'and cannot be used in requests.'); - } else if (!this.ready) { - throw new Error('ReliableMetadata is not ready.'); - } else if (this._collections.get(name)) { - throw new Error(`Collection "${name}" already exists.`); - } + create_collection(name) { + return Promise.resolve().then(() => { + if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { + throw new Error(`Collection "${name}" is reserved for internal use ` + + 'and cannot be used in requests.'); + } else if (!this.ready) { + throw new Error('ReliableMetadata is not ready.'); + } else if (this._collections.get(name)) { + throw new Error(`Collection "${name}" already exists.`); + } - const collection = new Collection(this._db, name); - this._collections.set(name, collection); + const collection = new Collection(this._db, name); + this._collections.set(name, collection); - create_collection(this._db, name, this._reliable_conn.connection()).then((res) => { + return create_collection(this._db, name, this._reliable_conn.connection()); + }).then((res) => { assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); logger.warn(`Collection created: "${name}"`); - collection._on_ready(done); + return new Promise((resolve, reject) => + collection._on_ready((maybeErr) => { + if (maybeErr instanceof Error) { + resolve(collection); + } else { + reject(maybeErr); + } + })); }).catch((err) => { if (collection._is_safe_to_remove()) { this._collections.delete(name); collection.close(); } - done(err); + throw err; }); } - handle_error(err, done) { - logger.debug(`Handling error: ${err.message}`); - try { - if (err instanceof error.CollectionNotReady) { - return err.collection._on_ready(done); - } else if (err instanceof error.IndexNotReady) { - return err.index.on_ready(done); - } else if (this._auto_create_collection && (err instanceof error.CollectionMissing)) { - logger.warn(`Auto-creating collection: ${err.name}`); - return this.create_collection(err.name, done); - } else if (this._auto_create_index && (err instanceof error.IndexMissing)) { - logger.warn(`Auto-creating index on collection "${err.collection.name}": ` + - `${JSON.stringify(err.fields)}`); - return err.collection._create_index(err.fields, this._reliable_conn.connection(), done); - } - done(err); - } catch (new_err) { - logger.debug(`Error when handling error: ${new_err.message}`); - done(new_err); - } - } - reliable_connection() { return this._reliable_conn; } diff --git a/server/src/metadata/table.js b/plugins/collection/src/table.js similarity index 100% rename from server/src/metadata/table.js rename to plugins/collection/src/table.js diff --git a/plugins/permit_all/.babelrc b/plugins/permit_all/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/permit_all/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/permit_all/package.json b/plugins/permit_all/package.json new file mode 100644 index 000000000..72415a09b --- /dev/null +++ b/plugins/permit_all/package.json @@ -0,0 +1,33 @@ +{ + "name": "@horizon/plugin-permit-all", + "version": "1.0.0", + "description": "Plugin that permits all Horizon operations.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/permit_all/src/index.js b/plugins/permit_all/src/index.js new file mode 100644 index 000000000..7e3723cb9 --- /dev/null +++ b/plugins/permit_all/src/index.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = (raw_config) => ({ + name: (raw_config && raw_config.name) || 'permissions', + + activate: () => ({ + methods: { + hz_permissions: { + type: 'preReq', + handler: (req, res, next) => { + req.validate = () => null; + next(); + }, + }, + }, + }), +}); diff --git a/plugins/reads/.babelrc b/plugins/reads/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/reads/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/reads/package.json b/plugins/reads/package.json new file mode 100644 index 000000000..9863b3dca --- /dev/null +++ b/plugins/reads/package.json @@ -0,0 +1,33 @@ +{ + "name": "@horizon/plugin-reads", + "version": "1.0.0", + "description": "Plugin that provides default Horizon read operations.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/reads/src/above.js b/plugins/reads/src/above.js new file mode 100644 index 000000000..e84619349 --- /dev/null +++ b/plugins/reads/src/above.js @@ -0,0 +1,17 @@ +'use strict'; + +const isObject = require('./common').isObject; + +module.exports = () => (req, res, next) => { + const args = req.options.above; + if (args.length < 1 || args.length > 2) { + next(new Error(`"above" expected 1 or 2 arguments but found ${args.length}.`)); + } else if (!isObject(args[0]) && typeof args[0] !== 'string') { + next(new Error('First argument to "above" must be a string or object.')); + } else if (args.length === 2 && (args[1] !== 'open' && args[1] !== 'closed')) { + next(new Error('Second argument to "above" must be "open" or "closed"')); + } else { + req.setParameter({value: args[0], bound: args.length === 1 ? 'open' : args[1]}); + next(); + } +}; diff --git a/plugins/reads/src/below.js b/plugins/reads/src/below.js new file mode 100644 index 000000000..764a6795f --- /dev/null +++ b/plugins/reads/src/below.js @@ -0,0 +1,17 @@ +'use strict'; + +const isObject = require('./common').isObject; + +module.exports = () => (req, res, next) => { + const args = req.options.below; + if (args.length < 1 || args.length > 2) { + next(new Error(`"below" expected 1 or 2 arguments but found ${args.length}.`)); + } else if (!isObject(args[0]) && typeof args[0] !== 'string') { + next(new Error('First argument to "below" must be a string or object.')); + } else if (args.length === 2 && (args[1] !== 'open' && args[1] !== 'closed')) { + next(new Error('Second argument to "below" must be "open" or "closed"')); + } else { + req.setParameter({value: args[0], bound: args.length === 1 ? 'closed' : args[1]}); + next(); + } +}; diff --git a/plugins/reads/src/common.js b/plugins/reads/src/common.js new file mode 100644 index 000000000..e2ae0a859 --- /dev/null +++ b/plugins/reads/src/common.js @@ -0,0 +1,131 @@ +'use strict'; + +const reql_options = { + timeFormat: 'raw', + binaryFormat: 'raw', +}; + +function isObject(x) { + return !Array.isArray(x) && x !== null; +} + +function object_to_fields(obj) { + return Object.keys(obj).map((key) => { + const value = obj[key]; + if (value !== null && typeof value === 'object' && !value.$reql_type$) { + return object_to_fields(value).map((subkeys) => [key].concat(subkeys)); + } else { + return [key]; + } + }); +} + +// This is exposed to be reused by 'subscribe' +const make_reql = (r, req) => Promise.resolve().then(() => { + const find = req.getParameter('find'); + const limit = req.getParameter('limit'); + const order = req.getParameter('order'); + const above = req.getParameter('above'); + const below = req.getParameter('below'); + const findAll = req.getParameter('findAll'); + const collection = req.getParameter('collection'); + + if (!collection) { + throw new Error('"collection" was not specified.'); + } else if (find && findAll) { + throw new Error('Cannot specify both "find" and "findAll".'); + } else if (find && (limit || order || above || below)) { + throw new Error('Cannot specify "find" with "limit", "order", "above", or "below".'); + } else if ((above || below) && !order) { + throw new Error('Cannot specify "above" or "below" without "order".'); + } + + const order_keys = (order && order[0]) || []; + let aboveKeyCount = above ? Object.keys(above[0]).length : 0; + let belowKeyCount = below ? Object.keys(below[0]).length : 0; + order_keys.forEach((k) => { + if (above) { + if (above[0][k] !== undefined) { + aboveKeyCount -= 1; + } else if (aboveKeyCount !== 0) { + throw new Error('The keys in "above" must appear continguously ' + + 'from the start of "order".'); + } + } + if (below) { + if (below[0][k] !== undefined) { + belowKeyCount -= 1; + } else if (belowKeyCount !== 0) { + throw new Error('The keys in "below" must appear continguously ' + + 'from the start of "order".'); + } + } + }); + + if (aboveKeyCount !== 0) { + throw new Error('The keys in "above" must all appear in "order".'); + } else if (belowKeyCount !== 0) { + throw new Error('The keys in "below" must all appear in "order".'); + } + + // RSI: this is all wrong + const ordered_between = (obj) => Promise.resolve().then(() => { + const fuzzy_fields = object_to_fields(obj); + return collection.get_matching_index(fuzzy_fields, order_keys); + }).then((index) => { + order_keys.forEach((k) => { + if (obj[k] !== undefined) { + throw new Error(`"${k}" cannot be used in "order", "above", or "below" ` + + 'when finding by that field.'); + } + }); + + const get_bound = (option) => { + const eval_key = (key) => { + if (obj[key] !== undefined) { + return obj[key]; + } else if (option && option[0][key] !== undefined) { + return option[0][key]; + } else if (option && option[1] === 'open') { + return option === above ? r.maxval : r.minval; + } else { + return option === above ? r.minval : r.maxval; + } + }; + + if (index.name === 'id') { + return eval_key('id'); + } + return index.fields.map((k) => eval_key(k)); + }; + + const above_value = get_bound(above); + const below_value = get_bound(below); + + const optargs = { + index: index.name, + leftBound: above ? above[1] : 'closed', + rightBound: below ? below[1] : 'closed', + }; + + return collection.table.orderBy({ + index: order && order[1] === 'descending' ? r.desc(index.name) : index.name, + }).between(above_value || r.minval, below_value || r.maxval, optargs); + }); + + let reqlPromise; + if (find) { + reqlPromise = ordered_between(find).then((subquery) => subquery.limit(1)); + } else if (findAll && findAll.length > 1) { + reqlPromise = Promise.all(findAll.map((x) => ordered_between(x))).then((subqueries) => + r.union.apply(subqueries)); + } else { + reqlPromise = ordered_between((findAll && findAll[0]) || {}); + } + + return reqlPromise.then((reql) => + limit !== undefined ? reql.limit(limit) : reql; + ); +}); + +module.exports = {make_reql, isObject, reql_options}; diff --git a/plugins/reads/src/fetch.js b/plugins/reads/src/fetch.js new file mode 100644 index 000000000..056c1e5f1 --- /dev/null +++ b/plugins/reads/src/fetch.js @@ -0,0 +1,52 @@ +'use strict'; + +const common = require('./common'); + +module.exports = (server) => (req, res, next) => { + const args = req.options.fetch; + const permissions = req.getParameter('permissions'); + const conn = server.rdb_connection().connection(); + + if (args.length !== 0) { + next(new Error(`"fetch" expects 0 arguments but found ${args.length}`)); + } else if (!permissions) { + next(new Error('"fetch" requires permissions to run')); + } else { + common.make_reql(server.r, req).then((reql) => + reql.run(conn, common.reql_options) + ).then((result) => { + if (result !== null && result.constructor.name === 'Cursor') { + res.complete.then(() => { + result.close().catch(() => { }); + }); + + // TODO: reuse cursor batching + return result.eachAsync((item) => { + const validator = permissions(); + if (validator && !validator(req.clientCtx, item)) { + next(new Error('Operation not permitted.')); + result.close().catch(() => { }); + } else { + res.write([item]); + } + }).then(() => { + res.end(); + }); + } else { + const validator = permissions(); + if (result !== null && result.constructor.name === 'Array') { + for (const item of result) { + if (validator && !validator(req.clientCtx, item)) { + return next(new Error('Operation not permitted.')); + } + } + res.end(result); + } else if (validator && !validator(req.clientCtx, result)) { + next(new Error('Operation not permitted.')); + } else { + res.end([result]); + } + } + }).catch(next); + } +}; diff --git a/plugins/reads/src/find.js b/plugins/reads/src/find.js new file mode 100644 index 000000000..2611123b3 --- /dev/null +++ b/plugins/reads/src/find.js @@ -0,0 +1,15 @@ +'use strict'; + +const isObject = require('./common').isObject; + +module.exports = () => (req, res, next) => { + const args = req.options.find; + if (args.length !== 1) { + next(new Error(`"find" expected 1 argument but found ${args.length}.`)); + } else if (!isObject(args[0])) { + next(new Error('First argument to "find" must be an object.')); + } else { + req.setParameter(args[0]); + next(); + } +}; diff --git a/plugins/reads/src/findAll.js b/plugins/reads/src/findAll.js new file mode 100644 index 000000000..755b1d1ad --- /dev/null +++ b/plugins/reads/src/findAll.js @@ -0,0 +1,15 @@ +'use strict'; + +const isObject = require('./common').isObject; + +module.exports = () => (req, res, next) => { + const args = req.options.findAll; + if (args.length < 1) { + next(new Error(`"findAll" expected 1 or more arguments but found ${args.length}.`)); + } else if (!args.every((val) => isObject(val))) { + next(new Error('All arguments to "findAll" must be objects.')); + } else { + req.setParameter(args); + next(); + } +}; diff --git a/plugins/reads/src/index.js b/plugins/reads/src/index.js new file mode 100644 index 000000000..c4bd40273 --- /dev/null +++ b/plugins/reads/src/index.js @@ -0,0 +1,52 @@ +'use strict'; + +const above = require('./above'); +const below = require('./below'); +const order = require('./order'); +const find = require('./find'); +const findAll = require('./findAll'); +const limit = require('./limit'); +const fetch = require('./fetch'); +const watch = require('./watch'); + +module.exports = (raw_config) => ({ + name: (raw_config && raw_config.name) || 'hz_reads', + activate: (server) => ({ + methods: { + above: { + type: 'option', + handler: above(server), + }, + below: { + type: 'option', + handler: below(server), + }, + order: { + type: 'option', + handler: order(server), + }, + find: { + type: 'option', + handler: find(server), + }, + findAll: { + type: 'option', + handler: findAll(server), + }, + limit: { + type: 'option', + handler: limit(server), + }, + fetch: { + requires: ['hz_permissions'], + type: 'terminal', + handler: fetch(server), + }, + watch: { + requires: ['hz_permissions'], + type: 'terminal', + handler: watch(server), + }, + }, + }), +}); diff --git a/plugins/reads/src/limit.js b/plugins/reads/src/limit.js new file mode 100644 index 000000000..daef98e86 --- /dev/null +++ b/plugins/reads/src/limit.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = () => (req, res, next) => { + const args = req.options.limit; + if (args.length !== 1) { + next(new Error(`"limit" expected 1 argument but found ${args.length}.`)); + } else if (typeof args[0] !== 'number') { + next(new Error('First argument to "limit" must be a number.')); + } else { + req.setParameter(args[0]); + next(); + } +}; diff --git a/plugins/reads/src/order.js b/plugins/reads/src/order.js new file mode 100644 index 000000000..dea80009f --- /dev/null +++ b/plugins/reads/src/order.js @@ -0,0 +1,31 @@ +'use strict'; + +function legalField(value) { + return typeof value === 'string' || + (Array.isArray(value) && value.every((i) => typeof i === 'string')); +} + +function convertField(value) { + return typeof value === 'string' ? [value] : value; +} + +module.exports = () => (req, res, next) => { + const args = req.options.order; + if (args.length < 1 || args.length > 2) { + next(new Error(`"order" expected 1 or 2 arguments but found ${args.length}.`)); + } else if (!Array.isArray(args[0]) && typeof args[0] !== 'string') { + next(new Error('First argument to "order" must be an array or string.')); + } else if (Array.isArray(args[0]) && !args[0].every(legalField)) { + next(new Error('First argument to "order" must be a string or ' + + 'an array of strings or arrays of strings.')); + } else if (args.length === 2 && + (args[1] !== 'ascending' && args[1] !== 'descending')) { + next(new Error('Second argument to "order" must be "ascending" or "descending"')); + } else { + req.setParameter({ + fields: Array.isArray(args[0]) ? args[0].map(convertField) : convertField(args[0]), + direction: args.length === 1 ? 'ascending' : args[1], + }); + next(); + } +}; diff --git a/plugins/reads/src/watch.js b/plugins/reads/src/watch.js new file mode 100644 index 000000000..96e4cfe07 --- /dev/null +++ b/plugins/reads/src/watch.js @@ -0,0 +1,48 @@ +'use strict'; + +const common = require('./common'); + +module.exports = (server) => (req, res, next) => { + const args = req.options.watch; + const permissions = req.getParameter('permissions'); + const conn = server.rdb_connection().connection(); + + if (args.length !== 0) { + next(new Error(`"watch" expects 0 arguments but found ${args.length}`)); + } else if (!permissions) { + next(new Error('"watch" requires permissions to run')); + } else { + common.make_reql(server.r, req).then((reql) => + reql.changes({ + include_initial: true, + include_states: true, + include_types: true, + include_offsets: + req.getParameter('order') !== undefined && + req.getParameter('limit') !== undefined, + }).run(conn, common.reql_options) + ).then((feed) => { + res.complete.then(() => { + feed.close().catch(() => { }); + }); + + // TODO: reuse cursor batches + return feed.eachAsync((item) => { + if (item.state === 'initializing') { + // Do nothing - we don't care + } else if (item.state === 'ready') { + res.write([], 'synced'); + } else { + const validator = permissions(); + if (validator) { + if ((item.old_val && !validator(req.clientCtx, item.old_val)) || + (item.new_val && !validator(req.clientCtx, item.new_val))) { + next(new Error('Operation not permitted.')); + } + } + res.write([item]); + } + }); + }).then(() => res.end()).catch(next); + } +}; diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json new file mode 100644 index 000000000..6c0125531 --- /dev/null +++ b/plugins/timeout/package.json @@ -0,0 +1,33 @@ +{ + "name": "@horizon/plugin-timeout", + "version": "1.0.0", + "description": "Plugin that converts timeouts to deadlines for Horizon write operations.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/timeout/src/index.js b/plugins/timeout/src/index.js new file mode 100644 index 000000000..67b6b3437 --- /dev/null +++ b/plugins/timeout/src/index.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = (raw_config) => ({ + name: (raw_config && raw_config.name) || 'hz_timeout', + activate: () => ({ + methods: ({ + timeout: { + type: 'option', + handler: (req, res, next) => { + const args = req.options.timeout; + if (args.length !== 1) { + next(new Error(`"timeout" expected 1 argument but found ${args.length}`)); + } else if (typeof args[0] !== 'number') { + next(new Error('timeout must be a number')); + } else { + req.setParameter(new Date() + args[0]); + next(); + } + }, + }, + }), + }), +}); diff --git a/plugins/writes/.babelrc b/plugins/writes/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/writes/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/writes/package.json b/plugins/writes/package.json new file mode 100644 index 000000000..098485845 --- /dev/null +++ b/plugins/writes/package.json @@ -0,0 +1,33 @@ +{ + "name": "@horizon/plugin-writes", + "version": "1.0.0", + "description": "Plugin that provides default Horizon write operations.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/writes/src/common.js b/plugins/writes/src/common.js new file mode 100644 index 000000000..645f836cd --- /dev/null +++ b/plugins/writes/src/common.js @@ -0,0 +1,198 @@ +'use strict'; + +const assert = require('assert'); + +const r = require('rethinkdb'); + +// Common functionality used by write requests + +const reql_options = { + timeFormat: 'raw', + binaryFormat: 'raw', +}; + +const invalidated_msg = 'Write invalidated by another request, try again.'; +const missing_msg = 'The document was missing.'; +const timeout_msg = 'Operation timed out.'; +const unauthorized_msg = 'Operation not permitted.'; + +const hz_v = '$hz_v$'; +function apply_version(row, new_version) { + row.merge(r.object(hz_v, new_version)); +} + +function make_write_response(data) { + data.forEach((item, index) => { + if (item instanceof Error) { + data[index] = {error: item.message}; + } + }); + return {data, state: 'complete'}; +} + +// This function returns a Promise that resolves to an array of responses - +// one for each row in `original_rows`, or rejects with an appropriate error. +// deadline -> a Date object for when to give up retrying, +// or a falsey value for no timeout +// pre_validate -> function (rows): +// rows: all pending rows +// return: an array or the promise of an array of info for those rows +// (which will be passed to the validate callback) +// validate_row -> function (row, info): +// validator: The function to validate with +// row: The row from the original query +// info: The info returned by the pre_validate step for this row +// return: nothing if successful or an error to be put as the response for this row +// do_write -> function (rows): +// rows: all pending rows +// return: a (promise of a) ReQL write result object +function retry_loop(original_rows, + permissions, + deadline, + pre_validate, + validate_row, + do_write) { + let first_attempt = true; + const iterate = (row_data_arg, response_data) => { + let row_data = row_data_arg; + if (row_data.length === 0) { + return response_data; + } else if (!first_attempt && deadline) { + if (Date.now() > deadline.getTime()) { + response_data.forEach((data, index) => { + if (data === null) { + response_data[index] = new Error(timeout_msg); + } + }); + return response_data; + } + } + + return Promise.resolve().then(() => { + // The validate callback may clobber the original version field in the row, + // so we have to restore it to the original value. + // This is done because validation only approves moving from one specific + // version of the row to another. Even if the original request did not choose + // the version, we are locked in to the version fetched from the pre_validate + // callback until the next iteration. If the version has changed in the meantime, + // it is an invalidated error which may be retried until we hit the deadline. + row_data.forEach((data) => { + if (data.version === undefined) { + delete data.row[hz_v]; + } else { + data.row[hz_v] = data.version; + } + }); + + // If permissions returns a function, we need to use it to validate + if (permissions()) { + // For the set of rows to write, gather info for the validation step + return Promise.resolve(pre_validate(row_data.map((data) => data.row))) + .then((infos) => { + assert(infos.length === row_data.length); + + // For each row to write (and info), validate it with permissions + const validator = permissions(); + if (validator) { + const valid_rows = []; + row_data.forEach((data, i) => { + const res = validate_row(validator, data.row, infos[i]); + + if (res !== undefined) { + response_data[data.index] = res; + } else { + valid_rows.push(data); + } + }); + row_data = valid_rows; + } + }); + } + }).then(() => { // For the set of valid rows, call the write step + if (row_data.length === 0) { + return []; + } + return do_write(row_data.map((data) => data.row)).then((res) => res.changes); + }).then((changes) => { + assert(changes.length === row_data.length); + + // Remove successful writes and invalidated writes that had an initial version + const retry_rows = []; + row_data.forEach((data, index) => { + const res = changes[index]; + if (res.error !== undefined) { + if (res.error.indexOf('Duplicate primary key') === 0) { + response_data[data.index] = {error: 'The document already exists.'}; + } else if (res.error.indexOf(invalidated_msg) === 0 && + data.version === undefined) { + retry_rows.push(data); + } else { + response_data[data.index] = {error: res.error}; + } + } else if (res.new_val === null) { + response_data[data.index] = {id: res.old_val.id, [hz_v]: res.old_val[hz_v]}; + } else { + response_data[data.index] = {id: res.new_val.id, [hz_v]: res.new_val[hz_v]}; + } + }); + + // Recurse, after which it will decide if there is more work to be done + first_attempt = false; + return iterate(retry_rows, response_data, deadline); + }); + }; + + return iterate(original_rows.map((row, index) => ({row, index, version: row[hz_v]})), + Array(original_rows.length).fill(null)) + .then(make_write_response); +} + +function validate_old_row_optional(validator, context, original, old_row, new_row) { + const expected_version = original[hz_v]; + if (expected_version !== undefined && + (!old_row || expected_version !== old_row[hz_v])) { + return new Error(invalidated_msg); + } else if (!validator(context, old_row, new_row)) { + return new Error(unauthorized_msg); + } + + if (old_row) { + const old_version = old_row[hz_v]; + if (expected_version === undefined) { + original[hz_v] = old_version === undefined ? -1 : old_version; + } + } +} + +function validate_old_row_required(validator, context, original, old_row, new_row) { + if (old_row == null) { + return new Error(missing_msg); + } + + const old_version = old_row[hz_v]; + const expected_version = original[hz_v]; + if (expected_version !== undefined && + expected_version !== old_version) { + return new Error(invalidated_msg); + } else if (!validator(context, old_row, new_row)) { + return new Error(unauthorized_msg); + } + + if (expected_version === undefined) { + original[hz_v] = old_version === undefined ? -1 : old_version; + } +} + +module.exports = { + invalidated_msg, + missing_msg, + timeout_msg, + unauthorized_msg, + make_write_response, + version_field: hz_v, + apply_version, + retry_loop, + validate_old_row_required, + validate_old_row_optional, + reql_options, +}; diff --git a/plugins/writes/src/index.js b/plugins/writes/src/index.js new file mode 100644 index 000000000..155385872 --- /dev/null +++ b/plugins/writes/src/index.js @@ -0,0 +1,46 @@ +'use strict'; + +const insert = require('./insert'); +const store = require('./store'); +const replace = require('./replace'); +const upsert = require('./upsert'); +const update = require('./update'); +const remove = require('./remove'); + +module.exports = (raw_config) => ({ + name: (raw_config && raw_config.name) || 'hz_writes', + activate: (server) => ({ + methods: { + insert: { + requires: ['hz_permissions'], + type: 'terminal', + handler: insert(server), + }, + store: { + requires: ['hz_permissions'], + type: 'terminal', + handler: store(server), + }, + replace: { + requires: ['hz_permissions'], + type: 'terminal', + handler: replace(server), + }, + upsert: { + requires: ['hz_permissions'], + type: 'terminal', + handler: upsert(server), + }, + update: { + requires: ['hz_permissions'], + type: 'terminal', + handler: update(server), + }, + remove: { + requires: ['hz_permissions'], + type: 'terminal', + handler: remove(server), + }, + }, + }), +}); diff --git a/plugins/writes/src/insert.js b/plugins/writes/src/insert.js new file mode 100644 index 000000000..638c9cbc3 --- /dev/null +++ b/plugins/writes/src/insert.js @@ -0,0 +1,32 @@ +'use strict'; + +const common = require('./common'); + +module.exports = (server) => (request, response, next) => { + const r = server.r; + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.insert, permissions, timeout, + (rows) => // pre-validation, all rows + Array(rows.length).fill(null), + (validator, row, info) => { // validation, each row + if (!validator(request.clientCtx, info, row)) { + return new Error(common.unauthorized_msg); + } + }, + (rows) => // write to database, all valid rows + collection.table + .insert(rows.map((row) => common.apply_version(r.expr(row), 0)), + {returnChanges: 'always'}) + .run(conn, common.reql_options) + ).then((msg) => response.end(msg)).catch(next); +}; diff --git a/plugins/writes/src/remove.js b/plugins/writes/src/remove.js new file mode 100644 index 000000000..edb8e2e5c --- /dev/null +++ b/plugins/writes/src/remove.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('./common'); +const hz_v = common.version_field; + +module.exports = (server) => (request, response, next) => { + const r = server.r; + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.remove, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows.map((row) => row.id)) + .map((id) => collection.table.get(id)) + .run(conn, common.reql_options), + (validator, row, info) => + common.validate_old_row_required(validator, request.clientCtx, row, info, null), + (rows) => // write to database, all valid rows + r.expr(rows).do((row_data) => + row_data.forEach((info) => + collection.table.get(info('id')).replace((row) => + r.branch(// The row may have been deleted between the get and now + row.eq(null), + null, + + // The row may have been changed between the get and now + r.and(info.hasFields(hz_v), + row(hz_v).default(-1).ne(info(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can safely remove the row + null), + + {returnChanges: 'always'})) + // Pretend like we deleted rows that didn't exist + .do((res) => + res.merge({changes: + r.range(row_data.count()).map((index) => + r.branch(res('changes')(index)('old_val').eq(null), + res('changes')(index).merge( + {old_val: {id: row_data(index)('id')}}), + res('changes')(index))).coerceTo('array'), + }))) + .run(conn, common.reql_options) + ).then((msg) => response.end(msg)).catch(next); +}; diff --git a/plugins/writes/src/replace.js b/plugins/writes/src/replace.js new file mode 100644 index 000000000..f7bb59777 --- /dev/null +++ b/plugins/writes/src/replace.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('./common'); +const hz_v = common.version_field; + +module.exports = (server) => (request, response, next) => { + const r = server.r; + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.replace, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows.map((row) => row.id)) + .map((id) => collection.table.get(id)) + .run(conn, common.reql_options), + (validator, row, info) => + common.validate_old_row_required(validator, request.clientCtx, row, info, row), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + collection.table.get(new_row('id')).replace((old_row) => + r.branch(// The row may have been deleted between the get and now + old_row.eq(null), + r.error(common.missing_msg), + + // The row may have been changed between the get and now + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can safely replace the row + common.apply_version(new_row, old_row(hz_v).default(-1).add(1))), + {returnChanges: 'always'})) + .run(conn, common.reql_options) + ).then((msg) => response.end(msg)).catch(next); +}; diff --git a/plugins/writes/src/store.js b/plugins/writes/src/store.js new file mode 100644 index 000000000..e629e82b5 --- /dev/null +++ b/plugins/writes/src/store.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('./common'); +const hz_v = common.version_field; + +module.exports = (server) => (request, response, next) => { + const r = server.r; + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.store, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows.map((row) => (row.id === undefined ? null : row.id))) + .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) + .run(conn, common.reql_options), + (validator, row, info) => + common.validate_old_row_optional(validator, request.clientCtx, row, info, row), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + r.branch(new_row.hasFields('id'), + collection.table.get(new_row('id')).replace((old_row) => + r.branch( + old_row.eq(null), + r.branch( + // Error if we were expecting the row to exist + new_row.hasFields(hz_v), + r.error(common.invalidated_msg), + + // Otherwise, insert the row + common.apply_version(new_row, 0) + ), + r.branch( + // The row may have changed from the expected version + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can overwrite the row + common.apply_version(new_row, + old_row(hz_v).default(-1).add(1)) + ) + ), {returnChanges: 'always'}), + + // The new row does not have an id, so we insert it with an autogen id + collection.table.insert(common.apply_version(new_row, 0), + {returnChanges: 'always'}))) + .run(conn, common.reql_options) + ).then((msg) => response.end(msg)).catch(next); +}; diff --git a/plugins/writes/src/update.js b/plugins/writes/src/update.js new file mode 100644 index 000000000..a500d5b3d --- /dev/null +++ b/plugins/writes/src/update.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('./common'); +const hz_v = common.version_field; + +module.exports = (server) => (request, response, next) => { + const r = server.r; + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.update, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows) + .map((new_row) => + collection.table.get(new_row('id')).do((old_row) => + r.branch(old_row.eq(null), + null, + [old_row, old_row.merge(new_row)]))) + .run(conn, common.reql_options), + (validator, row, info) => + common.validate_old_row_required(validator, request.clientCtx, row, info[0], info[1]), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + collection.table.get(new_row('id')).replace((old_row) => + r.branch(// The row may have been deleted between the get and now + old_row.eq(null), + r.error(common.missing_msg), + + // The row may have been changed between the get and now + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise we can update the row and increment the version + common.apply_version(old_row.merge(new_row), + old_row(hz_v).default(-1).add(1))), + {returnChanges: 'always'})) + .run(conn, common.reql_options) + ).then((msg) => response.end(msg)).catch(next); +}; diff --git a/plugins/writes/src/upsert.js b/plugins/writes/src/upsert.js new file mode 100644 index 000000000..2da2edbd3 --- /dev/null +++ b/plugins/writes/src/upsert.js @@ -0,0 +1,64 @@ +'use strict'; + +const common = require('./common'); +const hz_v = common.version_field; + +module.exports = (server) => (request, response, next) => { + const r = server.r; + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.upsert, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows) + .map((new_row) => + r.branch(new_row.hasFields('id'), + collection.table.get(new_row('id')).do((old_row) => + r.branch(old_row.eq(null), + [null, new_row], + [old_row, old_row.merge(new_row)])), + [null, new_row])) + .run(conn, common.reql_options), + (validator, row, info) => + common.validate_old_row_optional(validator, request.clientCtx, row, info[0], info[1]), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + r.branch(new_row.hasFields('id'), + collection.table.get(new_row('id')).replace((old_row) => + r.branch( + old_row.eq(null), + r.branch( + // Error if we were expecting the row to exist + new_row.hasFields(hz_v), + r.error(common.invalidated_msg), + + // Otherwise, insert the row + common.apply_version(new_row, 0) + ), + r.branch( + // The row may have changed from the expected version + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can update the row and increment the version + common.apply_version(old_row.merge(new_row), + old_row(hz_v).default(-1).add(1)) + ) + ), {returnChanges: 'always'}), + + // The new row did not have an id, so we insert it with an autogen id + collection.table.insert(common.apply_version(new_row, 0), + {returnChanges: 'always'}))) + .run(conn, common.reql_options) + ).then((msg) => response.end(msg)).catch(next); +}; diff --git a/protocol.md b/protocol.md index 6f27a89d7..d8023eb28 100644 --- a/protocol.md +++ b/protocol.md @@ -80,7 +80,7 @@ All requests match the following pattern: * The first argument is an object whose key-value pairs correspond to fields in `order`. * The second argument should be `closed` to include the boundary, and `open` otherwise. * `find` returns one object in `collection` that exactly matches the fields in the object given - optional. - * `find` cannot be used with `find_all`, `order`, `above`, or `below`. + * `find` cannot be used with `find_all`, `limit`, `order`, `above`, or `below`. * `find_all` is an array of objects whose key-value pairs correspond to keys in `index` - optional. * Returns any object in `collection` that exactly matches the fields in any of the objects given. * `find_all` cannot be used with `find`. diff --git a/server/src/client.js b/server/src/client.js index 865574dd3..b417adb18 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -2,7 +2,6 @@ const logger = require('./logger'); const schemas = require('./schema/horizon_protocol'); -const Request = require('./request'); const Response = require('./response'); const Joi = require('joi'); diff --git a/server/src/response.js b/server/src/response.js index 258f0dcfb..c3fce2d6f 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -1,5 +1,7 @@ 'use strict'; +const logger = require('./logger'); + class Response { constructor(socketSend) { this._socketSend = socketSend; @@ -11,6 +13,7 @@ class Response { this.write(data, 'complete'); }).catch((err) => { this._completed = true; + logger.debug(`Request failed with error: ${err.stack}`); this._socketSend({ error: `${err}`, error_code: err.code || -1, From bc7bdeed61789d0f0d832764740993e12dd35a4c Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 30 Aug 2016 18:16:56 -0700 Subject: [PATCH 40/86] checkpoint --- plugins/collection/src/collection.js | 32 ++++++++++++++------- plugins/collection/src/index.js | 4 +-- plugins/collection/src/reliable_metadata.js | 14 ++++++++- plugins/collection/src/table.js | 2 +- server/src/reliable.js | 9 +++++- 5 files changed, 45 insertions(+), 16 deletions(-) diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 7ec5500e4..cdb97c6e9 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -6,8 +6,9 @@ const Table = require('./table').Table; const r = require('rethinkdb'); class Collection { - constructor(db, name) { + constructor(db, name, reliableConn) { this.name = name; + this.reliableConn = reliableConn; this.table = r.db(db).table(name); // This is the ReQL Table object this._tables = new Map(); // A Map of Horizon Table objects this._registered = false; // Whether the `hz_collections` table says this collection exists @@ -71,20 +72,29 @@ class Collection { return this._tables.values().next().value; } - _create_index(fields, conn, done) { - return this._get_table().create_index(fields, conn, done); + _create_index(fields, done) { + return this._get_table().create_index(fields, this.reliableConn.connection(), done); } get_matching_index(fuzzy_fields, ordered_fields) { - const match = this._get_table().get_matching_index(fuzzy_fields, ordered_fields); - - if (match && !match.ready()) { - throw new error.IndexNotReady(this, match); - } else if (!match) { - throw new error.IndexMissing(this, fuzzy_fields.concat(ordered_fields)); - } + return new Promise((resolve, reject) => { + const done = (indexOrErr) => { + if (indexOrErr instanceof Error) { + reject(indexOrErr); + } else { + resolve(indexOrErr); + } + }; - return match; + const match = this._get_table().get_matching_index(fuzzy_fields, ordered_fields); + if (match && !match.ready()) { + match.on_ready(done); + } else if (!match) { + this._create_index(fuzzy_fields.concat(ordered_fields), done); + } else { + resolve(match); + } + }); } } diff --git a/plugins/collection/src/index.js b/plugins/collection/src/index.js index 0b5e8f7d0..d3c099834 100644 --- a/plugins/collection/src/index.js +++ b/plugins/collection/src/index.js @@ -123,7 +123,7 @@ class Index { table.indexWait(name).run(conn).then(() => { logger.debug(`${table} index ready: ${name}`); this._result = true; - this._waiters.forEach((w) => w()); + this._waiters.forEach((w) => w(this)); this._waiters = []; }).catch((err) => { this._result = err; @@ -149,7 +149,7 @@ class Index { if (this._result === true) { done(); } else if (this._result) { - done(this._result); + done(this); } else { this._waiters.push(done); } diff --git a/plugins/collection/src/reliable_metadata.js b/plugins/collection/src/reliable_metadata.js index ace56856c..fac89f2b4 100644 --- a/plugins/collection/src/reliable_metadata.js +++ b/plugins/collection/src/reliable_metadata.js @@ -320,8 +320,20 @@ class ReliableMetadata extends Reliable { const collection = this._collections.get(name); if (!collection && !this._auto_create_collection) { throw new Error(`Collection "${name}" does not exist.`); + } else if (collection) { + if (!collection.ready()) { + return new Promise((resolve, reject) => + collection._on_ready((maybeErr) => { + if (maybeErr instanceof Error) { + resolve(collection); + } else { + reject(maybeErr); + } + })); + } + return collection; } - return collection || this.create_collection(name); + return this.create_collection(name); }); } diff --git a/plugins/collection/src/table.js b/plugins/collection/src/table.js index 9ceead3bf..579c77c8d 100644 --- a/plugins/collection/src/table.js +++ b/plugins/collection/src/table.js @@ -1,4 +1,4 @@ -'use strict'; +use strict'; const index = require('./index'); const logger = require('../logger'); diff --git a/server/src/reliable.js b/server/src/reliable.js index c2afd157a..7fd32e085 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -111,10 +111,17 @@ class ReliableConn extends Reliable { }); } - connection() { + maybeConnection() { return this.ready ? this.conn : null; } + connection() { + if (!this.ready) { + throw new Error('Not connected'); + } + return this.conn; + } + close(reason) { let retProm = super.close(reason); if (this.conn) { From 128aa9514ebb34ac8e8cb05143908d07034f9e06 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 30 Aug 2016 21:30:15 -0700 Subject: [PATCH 41/86] checkpoint --- plugins/collection/src/main.js | 3 +- .../src/{reliable_metadata.js => metadata.js} | 0 server/src/auth.js | 4 - server/src/endpoint/common.js | 10 - server/src/endpoint/insert.js | 33 ---- server/src/endpoint/query.js | 137 ------------- server/src/endpoint/remove.js | 53 ----- server/src/endpoint/replace.js | 44 ----- server/src/endpoint/store.js | 57 ------ server/src/endpoint/subscribe.js | 41 ---- server/src/endpoint/update.js | 48 ----- server/src/endpoint/upsert.js | 63 ------ server/src/endpoint/writes.js | 182 ------------------ server/src/server.js | 4 +- 14 files changed, 4 insertions(+), 675 deletions(-) rename plugins/collection/src/{reliable_metadata.js => metadata.js} (100%) delete mode 100644 server/src/endpoint/common.js delete mode 100644 server/src/endpoint/insert.js delete mode 100644 server/src/endpoint/query.js delete mode 100644 server/src/endpoint/remove.js delete mode 100644 server/src/endpoint/replace.js delete mode 100644 server/src/endpoint/store.js delete mode 100644 server/src/endpoint/subscribe.js delete mode 100644 server/src/endpoint/update.js delete mode 100644 server/src/endpoint/upsert.js delete mode 100644 server/src/endpoint/writes.js diff --git a/plugins/collection/src/main.js b/plugins/collection/src/main.js index 7415f73c4..bab7f7f1c 100644 --- a/plugins/collection/src/main.js +++ b/plugins/collection/src/main.js @@ -18,7 +18,7 @@ module.exports = (raw_config) => ({ name: (raw_config && raw_config.name) || 'permissions', activate: (server) => { const metadata = new ReliableMetadata(...); - + // RSI: instantiate metadata return { methods: { collection: { @@ -26,6 +26,7 @@ module.exports = (raw_config) => ({ handler: collection(server, metadata), }, }, + deactivate: () => metadata.close(), }; }, }); diff --git a/plugins/collection/src/reliable_metadata.js b/plugins/collection/src/metadata.js similarity index 100% rename from plugins/collection/src/reliable_metadata.js rename to plugins/collection/src/metadata.js diff --git a/server/src/auth.js b/server/src/auth.js index 98c986f1d..23c3841bd 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -98,10 +98,6 @@ class Auth { generate(provider, info) { return Promise.resolve().then(() => { const conn = this._parent.rdb_connection().connection(); - if (!conn) { - throw new Error('No connection with the database.'); - } - const key = this.auth_key(provider, info); const db = r.db(this._parent.options.project_name); diff --git a/server/src/endpoint/common.js b/server/src/endpoint/common.js deleted file mode 100644 index c7eefae63..000000000 --- a/server/src/endpoint/common.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -const reql_options = { - timeFormat: 'raw', - binaryFormat: 'raw', -}; - -module.exports = { - reql_options, -}; diff --git a/server/src/endpoint/insert.js b/server/src/endpoint/insert.js deleted file mode 100644 index 04970f260..000000000 --- a/server/src/endpoint/insert.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -const insert = require('../schema/horizon_protocol').insert; -const writes = require('./writes'); -const reql_options = require('./common').reql_options; - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const run = (raw_request, context, ruleset, metadata, send, done) => { - const parsed = Joi.validate(raw_request.options, insert); - if (parsed.error !== null) { done(new Error(parsed.error.details[0].message)); } - - const collection = metadata.collection(parsed.value.collection); - const conn = metadata.connection(); - - writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout, - (rows) => // pre-validation, all rows - Array(rows.length).fill(null), - (row, info) => { // validation, each row - if (!ruleset.validate(context, info, row)) { - return new Error(writes.unauthorized_msg); - } - }, - (rows) => // write to database, all valid rows - collection.table - .insert(rows.map((row) => writes.apply_version(r.expr(row), 0)), - {returnChanges: 'always'}) - .run(conn, reql_options) - ).then(done).catch(done); -}; - -module.exports = {run}; diff --git a/server/src/endpoint/query.js b/server/src/endpoint/query.js deleted file mode 100644 index ab3f90b96..000000000 --- a/server/src/endpoint/query.js +++ /dev/null @@ -1,137 +0,0 @@ -'use strict'; - -const query = require('../schema/horizon_protocol').query; -const reql_options = require('./common').reql_options; - -const assert = require('assert'); - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const object_to_fields = (obj) => - Object.keys(obj).map((key) => { - const value = obj[key]; - if (value !== null && typeof value === 'object' && !value.$reql_type$) { - return object_to_fields(value).map((subkeys) => [key].concat(subkeys)); - } else { - return [key]; - } - }); - -// This is exposed to be reused by 'subscribe' -const make_reql = (raw_request, metadata) => { - const parsed = Joi.validate(raw_request.options, query); - if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); } - const options = parsed.value; - - const collection = metadata.collection(parsed.value.collection); - let reql = collection.table; - - const ordered_between = (obj) => { - const fuzzy_fields = object_to_fields(obj); - const order_keys = (options.order && options.order[0]) || - (options.above && Object.keys(options.above[0])) || - (options.below && Object.keys(options.below[0])) || []; - - if (order_keys.length >= 1) { - const k = order_keys[0]; - assert(!options.above || options.above[0][k] !== undefined, - '"above" must be on the same field as the first in "order".'); - assert(!options.below || options.below[0][k] !== undefined, - '"below" must be on the same field as the first in "order".'); - } - - order_keys.forEach((k) => { - assert(obj[k] === undefined, - `"${k}" cannot be used in "order", "above", or "below" when finding by that field.`); - }); - - const index = collection.get_matching_index(fuzzy_fields, order_keys.map((k) => [k])); - - const get_bound = (name) => { - const eval_key = (key) => { - if (obj[key] !== undefined) { - return obj[key]; - } else if (options[name] && options[name][0][key] !== undefined) { - return options[name][0][key]; - } else if (options[name] && options[name][1] === 'open') { - return name === 'above' ? r.maxval : r.minval; - } else { - return name === 'above' ? r.minval : r.maxval; - } - }; - - if (index.name === 'id') { - return eval_key('id'); - } - return index.fields.map((k) => eval_key(k)); - }; - - const above_value = get_bound('above'); - const below_value = get_bound('below'); - - const optargs = { - index: index.name, - leftBound: options.above ? options.above[1] : 'closed', - rightBound: options.below ? options.below[1] : 'closed', - }; - - const order = (options.order && options.order[1] === 'descending') ? - r.desc(index.name) : index.name; - return reql.orderBy({index: order}).between(above_value, below_value, optargs); - }; - - if (options.find) { - reql = ordered_between(options.find).limit(1); - } else if (options.find_all && options.find_all.length > 1) { - reql = r.union.apply(r, options.find_all.map((x) => ordered_between(x))); - } else { - reql = ordered_between((options.find_all && options.find_all[0]) || { }); - } - - if (options.limit !== undefined) { - reql = reql.limit(options.limit); - } - - return reql; -}; - -const run = (raw_request, context, ruleset, metadata, send, done) => { - let cursor; - const reql = make_reql(raw_request, metadata); - - reql.run(metadata.connection(), reql_options).then((res) => { - if (res !== null && res.constructor.name === 'Cursor') { - cursor = res; - return cursor.eachAsync((item) => { - if (!ruleset.validate(context, item)) { - done(new Error('Operation not permitted.')); - cursor.close().catch(() => { }); - } else { - send({data: [item]}); - } - }).then(() => { - done({data: [], state: 'complete'}); - }); - } else if (res !== null && res.constructor.name === 'Array') { - for (const item of res) { - if (!ruleset.validate(context, item)) { - return done(new Error('Operation not permitted.')); - } - } - done({data: res, state: 'complete'}); - } else if (!ruleset.validate(context, res)) { - done(new Error('Operation not permitted.')); - } else { - done({data: [res], state: 'complete'}); - } - }).catch(done); - - return () => { - if (cursor) { - cursor.close().catch(() => { }); - } - }; -}; - -module.exports = {make_reql, run}; diff --git a/server/src/endpoint/remove.js b/server/src/endpoint/remove.js deleted file mode 100644 index 372037df1..000000000 --- a/server/src/endpoint/remove.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -const remove = require('../schema/horizon_protocol').remove; -const reql_options = require('./common').reql_options; -const writes = require('./writes'); -const hz_v = writes.version_field; - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const run = (raw_request, context, ruleset, metadata, send, done) => { - const parsed = Joi.validate(raw_request.options, remove); - if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); } - - const collection = metadata.collection(parsed.value.collection); - const conn = metadata.connection(); - - writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout, - (rows) => // pre-validation, all rows - r.expr(rows.map((row) => row.id)) - .map((id) => collection.table.get(id)) - .run(conn, reql_options), - (row, info) => writes.validate_old_row_required(row, info, null, ruleset), - (rows) => // write to database, all valid rows - r.expr(rows).do((row_data) => - row_data.forEach((info) => - collection.table.get(info('id')).replace((row) => - r.branch(// The row may have been deleted between the get and now - row.eq(null), - null, - - // The row may have been changed between the get and now - r.and(info.hasFields(hz_v), - row(hz_v).default(-1).ne(info(hz_v))), - r.error(writes.invalidated_msg), - - // Otherwise, we can safely remove the row - null), - - {returnChanges: 'always'})) - // Pretend like we deleted rows that didn't exist - .do((res) => - res.merge({changes: - r.range(row_data.count()).map((index) => - r.branch(res('changes')(index)('old_val').eq(null), - res('changes')(index).merge({old_val: {id: row_data(index)('id')}}), - res('changes')(index))).coerceTo('array'), - }))) - .run(conn, reql_options) - ).then(done).catch(done); -}; - -module.exports = {run}; diff --git a/server/src/endpoint/replace.js b/server/src/endpoint/replace.js deleted file mode 100644 index 666a55049..000000000 --- a/server/src/endpoint/replace.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -const replace = require('../schema/horizon_protocol').replace; -const reql_options = require('./common').reql_options; -const writes = require('./writes'); -const hz_v = writes.version_field; - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const run = (raw_request, context, ruleset, metadata, send, done) => { - const parsed = Joi.validate(raw_request.options, replace); - if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); } - - const collection = metadata.collection(parsed.value.collection); - const conn = metadata.connection(); - - writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout, - (rows) => // pre-validation, all rows - r.expr(rows.map((row) => row.id)) - .map((id) => collection.table.get(id)) - .run(conn, reql_options), - (row, info) => writes.validate_old_row_required(row, info, row, ruleset), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - collection.table.get(new_row('id')).replace((old_row) => - r.branch(// The row may have been deleted between the get and now - old_row.eq(null), - r.error(writes.missing_msg), - - // The row may have been changed between the get and now - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), - - // Otherwise, we can safely replace the row - writes.apply_version(new_row, old_row(hz_v).default(-1).add(1))), - {returnChanges: 'always'})) - .run(conn, reql_options) - ).then(done).catch(done); -}; - -module.exports = {run}; diff --git a/server/src/endpoint/store.js b/server/src/endpoint/store.js deleted file mode 100644 index 0fe287667..000000000 --- a/server/src/endpoint/store.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict'; - -const store = require('../schema/horizon_protocol').store; -const reql_options = require('./common').reql_options; -const writes = require('./writes'); -const hz_v = writes.version_field; - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const run = (raw_request, context, ruleset, metadata, send, done) => { - const parsed = Joi.validate(raw_request.options, store); - if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); } - - const collection = metadata.collection(parsed.value.collection); - const conn = metadata.connection(); - - writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout, - (rows) => // pre-validation, all rows - r.expr(rows.map((row) => (row.id === undefined ? null : row.id))) - .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) - .run(conn, reql_options), - (row, info) => writes.validate_old_row_optional(row, info, row, ruleset), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).replace((old_row) => - r.branch( - old_row.eq(null), - r.branch( - // Error if we were expecting the row to exist - new_row.hasFields(hz_v), - r.error(writes.invalidated_msg), - - // Otherwise, insert the row - writes.apply_version(new_row, 0) - ), - r.branch( - // The row may have changed from the expected version - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), - - // Otherwise, we can safely overwrite the row - writes.apply_version(new_row, old_row(hz_v).default(-1).add(1)) - ) - ), {returnChanges: 'always'}), - - // The new row does not have an id, so we insert it with an autogen id - collection.table.insert(writes.apply_version(new_row, 0), - {returnChanges: 'always'}))) - .run(conn, reql_options) - ).then(done).catch(done); -}; - -module.exports = {run}; diff --git a/server/src/endpoint/subscribe.js b/server/src/endpoint/subscribe.js deleted file mode 100644 index 06af64120..000000000 --- a/server/src/endpoint/subscribe.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const make_reql = require('./query').make_reql; -const reql_options = require('./common').reql_options; - -const run = (raw_request, context, ruleset, metadata, send, done) => { - let feed; - const reql = make_reql(raw_request, metadata); - - reql.changes({include_initial: true, - include_states: true, - include_types: true, - include_offsets: Boolean(raw_request.options.order) && - Boolean(raw_request.options.limit)}) - .run(metadata.connection(), reql_options) - .then((res) => { - feed = res; - feed.eachAsync((item) => { - if (item.state === 'initializing') { - // Do nothing - we don't care - } else if (item.state === 'ready') { - send({state: 'synced'}); - } else if ((item.old_val && !ruleset.validate(context, item.old_val)) || - (item.new_val && !ruleset.validate(context, item.new_val))) { - throw new Error('Operation not permitted.'); - } else { - send({data: [item]}); - } - }).then(() => { - done({state: 'complete'}); - }).catch(done); - }).catch(done); - - return () => { - if (feed) { - feed.close().catch(() => { }); - } - }; -}; - -module.exports = {run}; diff --git a/server/src/endpoint/update.js b/server/src/endpoint/update.js deleted file mode 100644 index 2a37016b5..000000000 --- a/server/src/endpoint/update.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const update = require('../schema/horizon_protocol').update; -const reql_options = require('./common').reql_options; -const writes = require('./writes'); -const hz_v = writes.version_field; - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const run = (raw_request, context, ruleset, metadata, send, done) => { - const parsed = Joi.validate(raw_request.options, update); - if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); } - - const collection = metadata.collection(parsed.value.collection); - const conn = metadata.connection(); - - writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout, - (rows) => // pre-validation, all rows - r.expr(rows) - .map((new_row) => - collection.table.get(new_row('id')).do((old_row) => - r.branch(old_row.eq(null), - null, - [old_row, old_row.merge(new_row)]))) - .run(conn, reql_options), - (row, info) => writes.validate_old_row_required(row, info[0], info[1], ruleset), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - collection.table.get(new_row('id')).replace((old_row) => - r.branch(// The row may have been deleted between the get and now - old_row.eq(null), - r.error(writes.missing_msg), - - // The row may have been changed between the get and now - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), - - // Otherwise we can safely update the row and increment the version - writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1))), - {returnChanges: 'always'})) - .run(conn, reql_options) - ).then(done).catch(done); -}; - -module.exports = {run}; diff --git a/server/src/endpoint/upsert.js b/server/src/endpoint/upsert.js deleted file mode 100644 index 91df393a1..000000000 --- a/server/src/endpoint/upsert.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -const upsert = require('../schema/horizon_protocol').upsert; -const reql_options = require('./common').reql_options; -const writes = require('./writes'); -const hz_v = writes.version_field; - -const Joi = require('joi'); -const r = require('rethinkdb'); - -const run = (raw_request, context, ruleset, metadata, send, done) => { - const parsed = Joi.validate(raw_request.options, upsert); - if (parsed.error !== null) { throw new Error(parsed.error.details[0].message); } - - const collection = metadata.collection(parsed.value.collection); - const conn = metadata.connection(); - - writes.retry_loop(parsed.value.data, ruleset, parsed.value.timeout, - (rows) => // pre-validation, all rows - r.expr(rows) - .map((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).do((old_row) => - r.branch(old_row.eq(null), - [null, new_row], - [old_row, old_row.merge(new_row)])), - [null, new_row])) - .run(conn, reql_options), - (row, info) => writes.validate_old_row_optional(row, info[0], info[1], ruleset), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).replace((old_row) => - r.branch( - old_row.eq(null), - r.branch( - // Error if we were expecting the row to exist - new_row.hasFields(hz_v), - r.error(writes.invalidated_msg), - - // Otherwise, insert the row - writes.apply_version(new_row, 0) - ), - r.branch( - // The row may have changed from the expected version - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), - - // Otherwise, we can safely update the row and increment the version - writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1)) - ) - ), {returnChanges: 'always'}), - - // The new row did not have an id, so we insert it with an autogen id - collection.table.insert(writes.apply_version(new_row, 0), - {returnChanges: 'always'}))) - .run(conn, reql_options) - ).then(done).catch(done); -}; - -module.exports = {run}; diff --git a/server/src/endpoint/writes.js b/server/src/endpoint/writes.js deleted file mode 100644 index 8057127b9..000000000 --- a/server/src/endpoint/writes.js +++ /dev/null @@ -1,182 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -const r = require('rethinkdb'); - -// Common functionality used by write requests - -const invalidated_msg = 'Write invalidated by another request, try again.'; -const missing_msg = 'The document was missing.'; -const timeout_msg = 'Operation timed out.'; -const unauthorized_msg = 'Operation not permitted.'; - -const hz_v = '$hz_v$'; -const apply_version = (row, new_version) => row.merge(r.object(hz_v, new_version)); - -const make_write_response = (data) => { - data.forEach((item, index) => { - if (item instanceof Error) { - data[index] = {error: item.message}; - } - }); - return {data, state: 'complete'}; -}; - -// This function returns a Promise that resolves to an array of responses - one for each row in -// `original_rows`, or rejects with an appropriate error. -// timeout -> integer -// minimum number of milliseconds before giving up on retrying writes -// null means no timeout -// pre_validate -> function (rows): -// rows: all pending rows -// return: an array or the promise of an array of info for those rows -// (which will be passed to the validate callback) -// validate_row -> function (row, info): -// row: The row from the original query -// info: The info returned by the pre_validate step for this row -// return: nothing if successful or an error to be put as the response for this row -// do_write -> function (rows): -// rows: all pending rows -// return: a (promise of a) ReQL write result object -const retry_loop = (original_rows, ruleset, timeout, pre_validate, validate_row, do_write) => { - const iterate = (row_data_arg, response_data, deadline_optional) => { - let row_data = row_data_arg; - let deadline = deadline_optional; - if (row_data.length === 0) { - return response_data; - } else if (timeout !== null) { - if (!deadline) { - deadline = Date.now() + timeout; - } else if (Date.now() > deadline) { - response_data.forEach((data, index) => { - if (data === null) { - response_data[index] = new Error(timeout_msg); - } - }); - return response_data; - } - } - - - return Promise.resolve().then(() => { - // The validate callback may clobber the original version field in the row, - // so we have to restore it to the original value. - // This is done because validation only approves moving from one specific - // version of the row to another. Even if the original request did not choose - // the version, we are locked in to the version fetched from the pre_validate - // callback until the next iteration. If the version has changed in the meantime, - // it is an invalidated error which may be retried until we hit the deadline. - row_data.forEach((data) => { - if (data.version === undefined) { - delete data.row[hz_v]; - } else { - data.row[hz_v] = data.version; - } - }); - - if (ruleset.validation_required()) { - // For the set of rows to write, gather info for the validation step - return Promise.resolve(pre_validate(row_data.map((data) => data.row))).then((infos) => { - assert(infos.length === row_data.length); - - // For each row to write (and info), validate it with permissions - const valid_rows = []; - row_data.forEach((data, i) => { - const res = validate_row(data.row, infos[i]); - - if (res !== undefined) { - response_data[data.index] = res; - } else { - valid_rows.push(data); - } - }); - row_data = valid_rows; - }); - } - }).then(() => { // For the set of valid rows, call the write step - if (row_data.length === 0) { - return []; - } - return do_write(row_data.map((data) => data.row)).then((res) => res.changes); - }).then((changes) => { - assert(changes.length === row_data.length); - - // Remove successful writes and invalidated writes that had an initial version - const retry_rows = []; - row_data.forEach((data, index) => { - const res = changes[index]; - if (res.error !== undefined) { - if (res.error.indexOf('Duplicate primary key') === 0) { - response_data[data.index] = {error: 'The document already exists.'}; - } else if (res.error.indexOf(invalidated_msg) === 0 && - data.version === undefined) { - retry_rows.push(data); - } else { - response_data[data.index] = {error: res.error}; - } - } else if (res.new_val === null) { - response_data[data.index] = {id: res.old_val.id, [hz_v]: res.old_val[hz_v]}; - } else { - response_data[data.index] = {id: res.new_val.id, [hz_v]: res.new_val[hz_v]}; - } - }); - - // Recurse, after which it will decide if there is more work to be done - return iterate(retry_rows, response_data, deadline); - }); - }; - - return iterate(original_rows.map((row, index) => ({row, index, version: row[hz_v]})), - Array(original_rows.length).fill(null), - null).then(make_write_response); -}; - -const validate_old_row_optional = (original, old_row, new_row, ruleset) => { - const expected_version = original[hz_v]; - if (expected_version !== undefined && - (!old_row || expected_version !== old_row[hz_v])) { - return new Error(invalidated_msg); - } else if (!ruleset.validate(context, old_row, new_row)) { - return new Error(unauthorized_msg); - } - - if (old_row) { - const old_version = old_row[hz_v]; - if (expected_version === undefined) { - original[hz_v] = old_version === undefined ? -1 : old_version; - } - } -}; - -const validate_old_row_required = (original, old_row, new_row, ruleset) => { - if (old_row === null) { - return new Error(missing_msg); - } - - const old_version = old_row[hz_v]; - const expected_version = original[hz_v]; - if (expected_version !== undefined && - expected_version !== old_version) { - return new Error(invalidated_msg); - } else if (!ruleset.validate(context, old_row, new_row)) { - return new Error(unauthorized_msg); - } - - if (expected_version === undefined) { - original[hz_v] = old_version === undefined ? -1 : old_version; - } -}; - -module.exports = { - invalidated_msg, - missing_msg, - timeout_msg, - unauthorized_msg, - make_write_response, - version_field: hz_v, - apply_version, - retry_loop, - validate_old_row_required, - validate_old_row_optional, -}; diff --git a/server/src/server.js b/server/src/server.js index 313f4c8c6..20e36550c 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -142,8 +142,8 @@ class Server extends EventEmitter { // them to be closed. close() { if (!this._close_promise) { - this._close_promise = this._reliableMetadata.close().then( - () => Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { + this._close_promise = + Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { s.close(resolve); }))) ).then( From 77f3db82b15594293e791c8d168521564e120382 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Wed, 31 Aug 2016 06:00:33 +0000 Subject: [PATCH 42/86] working on stuff --- babelify.sh | 9 ++++++ plugin_router/.babelrc | 3 ++ plugin_router/package.json | 4 ++- plugin_router/src/index.js | 42 ++++++++++++++++++++++++++-- plugins/collection/.babelrc | 3 ++ plugins/collection/package.json | 2 +- plugins/collection/src/collection.js | 6 ++-- plugins/collection/src/main.js | 40 +++++++++++++++----------- plugins/collection/src/metadata.js | 34 ++++++++++++---------- plugins/collection/src/table.js | 21 +++++++------- plugins/permit_all/package.json | 2 +- plugins/reads/package.json | 2 +- plugins/reads/src/common.js | 9 +++--- plugins/timeout/package.json | 4 +-- plugins/writes/package.json | 2 +- plugins/writes/src/common.js | 4 +-- plugins/writes/src/insert.js | 2 +- plugins/writes/src/replace.js | 3 +- plugins/writes/src/store.js | 7 +++-- plugins/writes/src/update.js | 6 ++-- plugins/writes/src/upsert.js | 10 ++++--- server/src/server.js | 31 ++++++-------------- 22 files changed, 151 insertions(+), 95 deletions(-) create mode 100644 babelify.sh create mode 100644 plugin_router/.babelrc create mode 100644 plugins/collection/.babelrc diff --git a/babelify.sh b/babelify.sh new file mode 100644 index 000000000..08f8be380 --- /dev/null +++ b/babelify.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +for path in `find server plugins plugin_router -name .babelrc | grep -v node_modules`; do + { + babel ${path%%.babelrc}/src -d ${path%%.babelrc}/dist -s true -w + } & +done + +wait diff --git a/plugin_router/.babelrc b/plugin_router/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugin_router/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugin_router/package.json b/plugin_router/package.json index cce5efbb3..9034fab2e 100644 --- a/plugin_router/package.json +++ b/plugin_router/package.json @@ -26,6 +26,8 @@ "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3" + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" } } diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index cf0d3dc00..f491c75b0 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -2,14 +2,38 @@ const Request = require('./request'); +const EventEmitter = require('events'); const Toposort = require('toposort-class'); -class PluginRouter { +class PluginRouter extends EventEmitter { constructor(server) { + super(); this.server = server; - this.plugins = {}; this.httpRoutes = {}; this.methods = {}; + + this.plugins = new Set(); + this.readyPlugins = new Set(); + } + + noteReady(plugin) { + if (!this.readyPlugins.has(plugin)) { + this.readyPlugins.add(plugin); + this.emit('pluginReady', plugin, this); + if (this.readyPlugins.size === plugins.size) { + this.emit('ready', this); + } + } + } + + noteUnready(plugin) { + if (this.readyPlugins.has(plugin)) { + this.readyPlugins.delete(plugin); + this.emit('pluginUnready', plugin, this); + if (this.readyPlugins.size === plugins.size - 1) { + this.emit('unready', this); + } + } } add(plugin) { @@ -17,7 +41,19 @@ class PluginRouter { return Promise.reject( new Error(`Plugin conflict: "${plugin.name}" already present.`)); } - const activePlugin = Promise.resolve(this.server).then(plugin.activate); + const activePlugin = Promise.resolve(this.server).then((server) => { + if (plugin.activate.length > 1) { + return plugin.activate( + server, + () => this.noteReady(plugin.name), + () => this.noteUnready(plugin.name)); + } else { + return plugin.activate(server).then((x) => { + noteReady(plugin.name); + return x; + }); + } + }); this.plugins[plugin.name] = activePlugin.then((active) => { if (this.httpRoutes[plugin.name]) { throw new Error(`Plugin conflict: "${plugin.name}" already present.`); diff --git a/plugins/collection/.babelrc b/plugins/collection/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/collection/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/collection/package.json b/plugins/collection/package.json index 7d26ea8eb..c0233030b 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -2,7 +2,7 @@ "name": "@horizon/plugin-collection", "version": "1.0.0", "description": "Plugin that gets the collection for Horizon operations.", - "main": "src/index.js", + "main": "dist/main.js", "scripts": { "lint": "eslint src test", "test": "mocha test", diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index cdb97c6e9..ea51339f5 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -6,9 +6,11 @@ const Table = require('./table').Table; const r = require('rethinkdb'); class Collection { - constructor(db, name, reliableConn) { + constructor(db, name, reliableConn, logger, r) { this.name = name; this.reliableConn = reliableConn; + this.logger = logger; + this.r = r; this.table = r.db(db).table(name); // This is the ReQL Table object this._tables = new Map(); // A Map of Horizon Table objects this._registered = false; // Whether the `hz_collections` table says this collection exists @@ -29,7 +31,7 @@ class Collection { let table = this._tables.get(table_id); if (indexes) { if (!table) { - table = new Table(this.table, conn); + table = new Table(this.table, conn, this.logger, this.r); this._tables.set(table_id, table); } table.update_indexes(indexes, conn); diff --git a/plugins/collection/src/main.js b/plugins/collection/src/main.js index bab7f7f1c..81e481fd7 100644 --- a/plugins/collection/src/main.js +++ b/plugins/collection/src/main.js @@ -1,24 +1,32 @@ 'use strict'; -const collection = (server, metadata) => (req, res, next) => { - const args = req.options.collection; - if (args.length !== 1) { - next(new Error(`"collection" expected 1 argument but found ${args.length}.`)); - } else if (typeof args[0] !== 'string') { - next(new Error('First argument to "collection" must be a string.')); - } else { - metadata.collection(args[0]).then((collection) => { - req.setParameter(collection); - next(); - }).catch(next); - } -}; +function collection(server, metadata) { + return (req, res, next) => { + const args = req.options.collection; + if (args.length !== 1) { + next(new Error(`"collection" expected 1 argument but found ${args.length}.`)); + } else if (typeof args[0] !== 'string') { + next(new Error('First argument to "collection" must be a string.')); + } else { + metadata.collection(args[0]).then((collection) => { + req.setParameter(collection); + next(); + }).catch(next); + } + }; +} module.exports = (raw_config) => ({ name: (raw_config && raw_config.name) || 'permissions', - activate: (server) => { - const metadata = new ReliableMetadata(...); - // RSI: instantiate metadata + // RSI: make sure we check the arity and become ready if people + // don't take the callbacks. + activate: (server, onReady, onUnready) => { + const metadata = new ReliableMetadata( + server.options.project_name, + server.rdb_connection(), + raw_config.auto_create_collection, + raw_config.auto_create_index); + metadata.subscribe({onReady, onUnready}); return { methods: { collection: { diff --git a/plugins/collection/src/metadata.js b/plugins/collection/src/metadata.js index fac89f2b4..c8733036a 100644 --- a/plugins/collection/src/metadata.js +++ b/plugins/collection/src/metadata.js @@ -1,19 +1,15 @@ 'use strict'; -const error = require('../error'); const {Reliable, ReliableChangefeed, ReliableUnion} = require('../reliable'); -const logger = require('../logger'); const Collection = require('./collection').Collection; const utils = require('../utils'); const assert = require('assert'); -const r = require('rethinkdb'); - const version_field = '$hz_v$'; const metadata_version = [2, 0, 0]; -const create_collection = (db, name, conn) => +const create_collection = (r, db, name, conn) => r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => r.branch( res('errors').ne(0), @@ -24,7 +20,7 @@ const create_collection = (db, name, conn) => ) ).run(conn); -const initialize_metadata = (db, conn) => +const initialize_metadata = (r, db, conn) => r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) .then(() => Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => @@ -37,7 +33,7 @@ const initialize_metadata = (db, conn) => .then(() => Promise.all([ r.db(db).tableList().contains('users').not().run(conn).then(() => - create_collection(db, 'users', conn)), + create_collection(r, db, 'users', conn)), r.db(db).table('hz_collections') .insert({id: 'hz_metadata', version: metadata_version}) .run(conn), @@ -46,6 +42,7 @@ const initialize_metadata = (db, conn) => class StaleAttemptError extends Error { } +// RSI: fix all this shit. class ReliableInit extends Reliable { constructor(db, reliable_conn, auto_create_collection) { super(); @@ -92,7 +89,7 @@ class ReliableInit extends Reliable { this.check_attempt(attempt); logger.debug('checking for internal tables'); if (this._auto_create_collection) { - return initialize_metadata(this._db, conn); + return initialize_metadata(this.r, this._db, conn); } else { return r.dbList().contains(this._db).run(conn).then((has_db) => { if (!has_db) { @@ -163,8 +160,12 @@ class ReliableMetadata extends Reliable { constructor(project_name, reliable_conn, auto_create_collection, - auto_create_index) { + auto_create_index, + logger, + r) { super(); + this.logger = logger; + this.r = r; this._db = project_name; this._reliable_conn = reliable_conn; @@ -200,7 +201,8 @@ class ReliableMetadata extends Reliable { const collection_name = change.new_val.id; let collection = this._collections.get(collection_name); if (!collection) { - collection = new Collection(this._db, collection_name); + collection = new Collection( + this._db, collection_name, this.logger, this.r); this._collections.set(collection_name, collection); } collection._register(); @@ -253,10 +255,12 @@ class ReliableMetadata extends Reliable { let collection = this._collections.get(collection_name); if (!collection) { - collection = new Collection(this._db, collection_name); + collection = new Collection( + this._db, collection_name, this.logger, this.r); this._collections.set(collection_name, collection); } - collection._update_table(table_id, change.new_val.indexes, this._connection); + collection._update_table( + table_id, change.new_val.indexes, this._connection); } break; case 'uninitial': @@ -348,13 +352,13 @@ class ReliableMetadata extends Reliable { throw new Error(`Collection "${name}" already exists.`); } - const collection = new Collection(this._db, name); + const collection = new Collection(this._db, name, this.logger, this.r); this._collections.set(name, collection); - return create_collection(this._db, name, this._reliable_conn.connection()); + return create_collection(this.r, this._db, name, this._reliable_conn.connection()); }).then((res) => { assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); - logger.warn(`Collection created: "${name}"`); + this.logger.warn(`Collection created: "${name}"`); return new Promise((resolve, reject) => collection._on_ready((maybeErr) => { if (maybeErr instanceof Error) { diff --git a/plugins/collection/src/table.js b/plugins/collection/src/table.js index 579c77c8d..99b9b3e1a 100644 --- a/plugins/collection/src/table.js +++ b/plugins/collection/src/table.js @@ -1,15 +1,13 @@ -use strict'; +'use strict'; const index = require('./index'); -const logger = require('../logger'); - const assert = require('assert'); -const r = require('rethinkdb'); - class Table { - constructor(reql_table, conn) { + constructor(reql_table, conn, logger, r) { this.table = reql_table; + this.logger = logger; + this.r = r; this.indexes = new Map(); this._waiters = []; @@ -52,7 +50,7 @@ class Table { } update_indexes(indexes, conn) { - logger.debug(`${this.table} indexes changed, reevaluating`); + this.logger.debug(`${this.table} indexes changed, reevaluating`); // Initialize the primary index, which won't show up in the changefeed indexes.push(index.primary_index_name); @@ -69,13 +67,13 @@ class Table { } new_index_map.set(name, new_index); } catch (err) { - logger.warn(`${err}`); + this.logger.warn(`${err}`); } }); this.indexes.forEach((i) => i.close()); this.indexes = new_index_map; - logger.debug(`${this.table} indexes updated`); + this.logger.debug(`${this.table} indexes updated`); } // TODO: support geo and multi indexes @@ -88,7 +86,8 @@ class Table { // Create the Index object now so we don't try to create it again before the // feed notifies us of the index creation const new_index = new index.Index(index_name, this.table, conn); - this.indexes.set(index_name, new_index); // TODO: shouldn't this be done before we go async? + // TODO: shouldn't this be done before we go async? + this.indexes.set(index_name, new_index); return new_index.on_ready(done); }; @@ -97,7 +96,7 @@ class Table { .run(conn) .then(success) .catch((err) => { - if (err instanceof r.Error.ReqlError && + if (err instanceof this.r.Error.ReqlError && err.msg.indexOf('already exists') !== -1) { success(); } else { diff --git a/plugins/permit_all/package.json b/plugins/permit_all/package.json index 72415a09b..db6cfb16d 100644 --- a/plugins/permit_all/package.json +++ b/plugins/permit_all/package.json @@ -2,7 +2,7 @@ "name": "@horizon/plugin-permit-all", "version": "1.0.0", "description": "Plugin that permits all Horizon operations.", - "main": "src/index.js", + "main": "dist/index.js", "scripts": { "lint": "eslint src test", "test": "mocha test", diff --git a/plugins/reads/package.json b/plugins/reads/package.json index 9863b3dca..dd96c1e13 100644 --- a/plugins/reads/package.json +++ b/plugins/reads/package.json @@ -2,7 +2,7 @@ "name": "@horizon/plugin-reads", "version": "1.0.0", "description": "Plugin that provides default Horizon read operations.", - "main": "src/index.js", + "main": "dist/index.js", "scripts": { "lint": "eslint src test", "test": "mocha test", diff --git a/plugins/reads/src/common.js b/plugins/reads/src/common.js index e2ae0a859..09cffd447 100644 --- a/plugins/reads/src/common.js +++ b/plugins/reads/src/common.js @@ -117,15 +117,14 @@ const make_reql = (r, req) => Promise.resolve().then(() => { if (find) { reqlPromise = ordered_between(find).then((subquery) => subquery.limit(1)); } else if (findAll && findAll.length > 1) { - reqlPromise = Promise.all(findAll.map((x) => ordered_between(x))).then((subqueries) => - r.union.apply(subqueries)); + reqlPromise = Promise.all( + findAll.map((x) => ordered_between(x))).then((subqueries) => + r.union.apply(subqueries)); } else { reqlPromise = ordered_between((findAll && findAll[0]) || {}); } - return reqlPromise.then((reql) => - limit !== undefined ? reql.limit(limit) : reql; - ); + return reqlPromise.then((reql) => (limit !== undefined ? reql.limit(limit) : reql)); }); module.exports = {make_reql, isObject, reql_options}; diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json index 6c0125531..e908d1330 100644 --- a/plugins/timeout/package.json +++ b/plugins/timeout/package.json @@ -1,8 +1,8 @@ { "name": "@horizon/plugin-timeout", "version": "1.0.0", - "description": "Plugin that converts timeouts to deadlines for Horizon write operations.", - "main": "src/index.js", + "description": "Converts timeouts to deadlines for Horizon write operations.", + "main": "dist/index.js", "scripts": { "lint": "eslint src test", "test": "mocha test", diff --git a/plugins/writes/package.json b/plugins/writes/package.json index 098485845..4f2c3d062 100644 --- a/plugins/writes/package.json +++ b/plugins/writes/package.json @@ -2,7 +2,7 @@ "name": "@horizon/plugin-writes", "version": "1.0.0", "description": "Plugin that provides default Horizon write operations.", - "main": "src/index.js", + "main": "dist/index.js", "scripts": { "lint": "eslint src test", "test": "mocha test", diff --git a/plugins/writes/src/common.js b/plugins/writes/src/common.js index 645f836cd..d1505280c 100644 --- a/plugins/writes/src/common.js +++ b/plugins/writes/src/common.js @@ -2,8 +2,6 @@ const assert = require('assert'); -const r = require('rethinkdb'); - // Common functionality used by write requests const reql_options = { @@ -17,7 +15,7 @@ const timeout_msg = 'Operation timed out.'; const unauthorized_msg = 'Operation not permitted.'; const hz_v = '$hz_v$'; -function apply_version(row, new_version) { +function apply_version(r, row, new_version) { row.merge(r.object(hz_v, new_version)); } diff --git a/plugins/writes/src/insert.js b/plugins/writes/src/insert.js index 638c9cbc3..b5e3a4bb7 100644 --- a/plugins/writes/src/insert.js +++ b/plugins/writes/src/insert.js @@ -25,7 +25,7 @@ module.exports = (server) => (request, response, next) => { }, (rows) => // write to database, all valid rows collection.table - .insert(rows.map((row) => common.apply_version(r.expr(row), 0)), + .insert(rows.map((row) => common.apply_version(r, r.expr(row), 0)), {returnChanges: 'always'}) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/writes/src/replace.js b/plugins/writes/src/replace.js index f7bb59777..07bb3a555 100644 --- a/plugins/writes/src/replace.js +++ b/plugins/writes/src/replace.js @@ -37,7 +37,8 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, we can safely replace the row - common.apply_version(new_row, old_row(hz_v).default(-1).add(1))), + common.apply_version( + r, new_row, old_row(hz_v).default(-1).add(1))), {returnChanges: 'always'})) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/writes/src/store.js b/plugins/writes/src/store.js index e629e82b5..63b89ab12 100644 --- a/plugins/writes/src/store.js +++ b/plugins/writes/src/store.js @@ -36,7 +36,7 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, insert the row - common.apply_version(new_row, 0) + common.apply_version(r, new_row, 0) ), r.branch( // The row may have changed from the expected version @@ -45,13 +45,14 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, we can overwrite the row - common.apply_version(new_row, + common.apply_version(r, + new_row, old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row does not have an id, so we insert it with an autogen id - collection.table.insert(common.apply_version(new_row, 0), + collection.table.insert(common.apply_version(r, new_row, 0), {returnChanges: 'always'}))) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/writes/src/update.js b/plugins/writes/src/update.js index a500d5b3d..6da8ceb2e 100644 --- a/plugins/writes/src/update.js +++ b/plugins/writes/src/update.js @@ -26,7 +26,8 @@ module.exports = (server) => (request, response, next) => { [old_row, old_row.merge(new_row)]))) .run(conn, common.reql_options), (validator, row, info) => - common.validate_old_row_required(validator, request.clientCtx, row, info[0], info[1]), + common.validate_old_row_required( + validator, request.clientCtx, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => @@ -41,7 +42,8 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise we can update the row and increment the version - common.apply_version(old_row.merge(new_row), + common.apply_version(r, + old_row.merge(new_row), old_row(hz_v).default(-1).add(1))), {returnChanges: 'always'})) .run(conn, common.reql_options) diff --git a/plugins/writes/src/upsert.js b/plugins/writes/src/upsert.js index 2da2edbd3..c8462ce27 100644 --- a/plugins/writes/src/upsert.js +++ b/plugins/writes/src/upsert.js @@ -28,7 +28,8 @@ module.exports = (server) => (request, response, next) => { [null, new_row])) .run(conn, common.reql_options), (validator, row, info) => - common.validate_old_row_optional(validator, request.clientCtx, row, info[0], info[1]), + common.validate_old_row_optional( + validator, request.clientCtx, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => @@ -42,7 +43,7 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, insert the row - common.apply_version(new_row, 0) + common.apply_version(r, new_row, 0) ), r.branch( // The row may have changed from the expected version @@ -51,13 +52,14 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, we can update the row and increment the version - common.apply_version(old_row.merge(new_row), + common.apply_version(r, + old_row.merge(new_row), old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row did not have an id, so we insert it with an autogen id - collection.table.insert(common.apply_version(new_row, 0), + collection.table.insert(common.apply_version(r, new_row, 0), {returnChanges: 'always'}))) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/server/src/server.js b/server/src/server.js index 20e36550c..ddc7faeb3 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -4,7 +4,6 @@ const Auth = require('./auth').Auth; const ClientConnection = require('./client'); const logger = require('./logger'); const {ReliableConn, ReliableChangefeed} = require('./reliable'); -const {ReliableMetadata} = require('./metadata/reliable_metadata'); const optionsSchema = require('./schema/server_options').server; const EventEmitter = require('events'); @@ -49,17 +48,11 @@ class Server extends EventEmitter { this.r = r; this.logger = logger; - this.ReliableChangefeed = ReliableChangefeed; // RSI: better place to put this that on the context? Should plugins require the server? + // RSI: better place to put this that on the context? Should plugins + // require the server? + this.ReliableChangefeed = ReliableChangefeed; - // TODO: consider emitting errors sometimes. - this._reliableMetadata = new ReliableMetadata( - this.options.project_name, - this._reliableConn, - this._clients, - this.options.auto_create_collection, - this.options.auto_create_index); - - this._clear_clients_subscription = this._reliableMetadata.subscribe({ + this._clear_clients_subscription = this._reliableConn.subscribe({ onReady: () => { this.emit('ready', this); }, @@ -73,7 +66,7 @@ class Server extends EventEmitter { const verifyClient = (info, cb) => { // Reject connections if we aren't synced with the database - if (!this._reliableMetadata.ready) { + if (!this._reliableConn.ready) { cb(false, 503, 'Connection to the database is down.'); } else { cb(true); @@ -82,13 +75,14 @@ class Server extends EventEmitter { const ws_options = {handleProtocols, verifyClient, path: this.options.path}; - // RSI: only become ready when this and metadata are both ready. + // RSI: only become ready when the websocket servers and the + // connection are both ready. const add_websocket = (server) => { const ws_server = new websocket.Server(Object.assign({server}, ws_options)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => { try { - if (!this._reliableMetadata.ready) { + if (!this._reliableConn.ready) { throw new Error('No connection to the database.'); } @@ -126,10 +120,6 @@ class Server extends EventEmitter { return this._auth; } - metadata() { - return this._reliableMetadata; - } - rdb_connection() { return this._reliableConn; } @@ -145,10 +135,7 @@ class Server extends EventEmitter { this._close_promise = Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { s.close(resolve); - }))) - ).then( - () => this._reliableConn.close() - ); + }))).then(() => this._reliableConn.close()); } return this._close_promise; } From 7f7b975cd194d0094722efaece97035c835eccf3 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Thu, 1 Sep 2016 00:40:08 +0000 Subject: [PATCH 43/86] collection broken --- babelify.sh | 3 +- plugin_router/src/index.js | 28 ++--- plugins/collection/package.json | 1 + plugins/collection/src/collection.js | 3 - plugins/collection/src/index.js | 3 +- plugins/collection/src/main.js | 45 +++---- plugins/collection/src/metadata.js | 30 ++--- plugins/collection/src/table.js | 4 +- {server => plugins/collection}/src/utils.js | 0 plugins/timeout/.babelrc | 3 + server-utils/src/reliable.js | 117 +++++++++++++++++ server/package.json | 1 + server/src/reliable.js | 133 +------------------- server/src/schema/server_options.js | 6 - server/src/server.js | 4 + test/serve.js | 45 ++++--- 16 files changed, 212 insertions(+), 214 deletions(-) rename {server => plugins/collection}/src/utils.js (100%) create mode 100644 plugins/timeout/.babelrc create mode 100644 server-utils/src/reliable.js diff --git a/babelify.sh b/babelify.sh index 08f8be380..07db60d20 100644 --- a/babelify.sh +++ b/babelify.sh @@ -1,6 +1,7 @@ #!/bin/bash -for path in `find server plugins plugin_router -name .babelrc | grep -v node_modules`; do +dirs="server server-utils plugins plugin_router" +for path in `find $dirs -name .babelrc | grep -v node_modules`; do { babel ${path%%.babelrc}/src -d ${path%%.babelrc}/dist -s true -w } & diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index f491c75b0..9400779c3 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -1,9 +1,8 @@ 'use strict'; const Request = require('./request'); - -const EventEmitter = require('events'); const Toposort = require('toposort-class'); +const EventEmitter = require('events'); class PluginRouter extends EventEmitter { constructor(server) { @@ -12,7 +11,7 @@ class PluginRouter extends EventEmitter { this.httpRoutes = {}; this.methods = {}; - this.plugins = new Set(); + this.plugins = new Map(); this.readyPlugins = new Set(); } @@ -20,8 +19,8 @@ class PluginRouter extends EventEmitter { if (!this.readyPlugins.has(plugin)) { this.readyPlugins.add(plugin); this.emit('pluginReady', plugin, this); - if (this.readyPlugins.size === plugins.size) { - this.emit('ready', this); + if (this.readyPlugins.size === this.plugins.size) { + setImmediate(() => this.emit('ready', this)); } } } @@ -30,31 +29,32 @@ class PluginRouter extends EventEmitter { if (this.readyPlugins.has(plugin)) { this.readyPlugins.delete(plugin); this.emit('pluginUnready', plugin, this); - if (this.readyPlugins.size === plugins.size - 1) { + if (this.readyPlugins.size === this.plugins.size - 1) { this.emit('unready', this); } } } add(plugin) { - if (this.plugins[plugin.name]) { + if (this.plugins.has(plugin.name)) { return Promise.reject( new Error(`Plugin conflict: "${plugin.name}" already present.`)); } const activePlugin = Promise.resolve(this.server).then((server) => { + this.emit('unready', this); if (plugin.activate.length > 1) { return plugin.activate( server, () => this.noteReady(plugin.name), () => this.noteUnready(plugin.name)); } else { - return plugin.activate(server).then((x) => { - noteReady(plugin.name); + return Promise.resolve().then(() => plugin.activate(server)).then((x) => { + this.noteReady(plugin.name); return x; }); } }); - this.plugins[plugin.name] = activePlugin.then((active) => { + this.plugins.set(plugin.name, activePlugin.then((active) => { if (this.httpRoutes[plugin.name]) { throw new Error(`Plugin conflict: "${plugin.name}" already present.`); } @@ -75,15 +75,15 @@ class PluginRouter extends EventEmitter { this.server.logger.error(`Error when adding plugin ${plugin.name}: ${err}`); this.server.logger.debug(`${err.stack}`); throw err; - }); - return this.plugins[plugin.name]; + })); + return this.plugins.get(plugin.name); } remove(plugin, reason) { - if (!this.plugins[plugin.name]) { + if (!this.plugins.has(plugin.name)) { return Promise.reject(new Error(`Plugin "${plugin.name}" is not present.`)); } - return this.plugins[plugin.name].then((active) => { + return this.plugins.get(plugin.name).then((active) => { for (const m in active.methods) { delete this.methods[m]; this._requirementsOrdering = null; diff --git a/plugins/collection/package.json b/plugins/collection/package.json index c0233030b..d199d9323 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -22,6 +22,7 @@ "node": ">=4.0.0" }, "dependencies": { + "@horizon/server-utils": "1.0.0" }, "devDependencies": { "eslint": "^2.3.0", diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index ea51339f5..cb47c4d20 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -1,10 +1,7 @@ 'use strict'; -const error = require('../error'); const Table = require('./table').Table; -const r = require('rethinkdb'); - class Collection { constructor(db, name, reliableConn, logger, r) { this.name = name; diff --git a/plugins/collection/src/index.js b/plugins/collection/src/index.js index d3c099834..5308cc3c9 100644 --- a/plugins/collection/src/index.js +++ b/plugins/collection/src/index.js @@ -1,7 +1,6 @@ 'use strict'; const assert = require('assert'); -const logger = require('../logger'); // Index names are of the format "hz_[_]" where may be // omitted or "multi_" or "geo" (at the moment). is a JSON array @@ -102,7 +101,7 @@ const compare_fields = (a, b) => { }; class Index { - constructor(name, table, conn) { + constructor(logger, name, table, conn) { logger.debug(`${table} index registered: ${name}`); const info = name_to_info(name); this.name = name; diff --git a/plugins/collection/src/main.js b/plugins/collection/src/main.js index 81e481fd7..48db8f792 100644 --- a/plugins/collection/src/main.js +++ b/plugins/collection/src/main.js @@ -1,5 +1,7 @@ 'use strict'; +import {ReliableMetadata} from './metadata.js'; + function collection(server, metadata) { return (req, res, next) => { const args = req.options.collection; @@ -16,25 +18,26 @@ function collection(server, metadata) { }; } -module.exports = (raw_config) => ({ - name: (raw_config && raw_config.name) || 'permissions', - // RSI: make sure we check the arity and become ready if people - // don't take the callbacks. - activate: (server, onReady, onUnready) => { - const metadata = new ReliableMetadata( - server.options.project_name, - server.rdb_connection(), - raw_config.auto_create_collection, - raw_config.auto_create_index); - metadata.subscribe({onReady, onUnready}); - return { - methods: { - collection: { - type: 'option', - handler: collection(server, metadata), +export default function(raw_config) { + return { + name: (raw_config && raw_config.name) || 'permissions', + // RSI: make sure we check the arity and become ready if people + // don't take the callbacks. + activate: (server, onReady, onUnready) => { + const metadata = new ReliableMetadata( + server, + raw_config.auto_create_collection, + raw_config.auto_create_index); + metadata.subscribe({onReady, onUnready}); + return { + methods: { + collection: { + type: 'option', + handler: collection(server, metadata), + }, }, - }, - deactivate: () => metadata.close(), - }; - }, -}); + deactivate: () => metadata.close(), + }; + }, + }; +} diff --git a/plugins/collection/src/metadata.js b/plugins/collection/src/metadata.js index c8733036a..d4e6d4a23 100644 --- a/plugins/collection/src/metadata.js +++ b/plugins/collection/src/metadata.js @@ -1,15 +1,15 @@ 'use strict'; -const {Reliable, ReliableChangefeed, ReliableUnion} = require('../reliable'); +import {Reliable, ReliableUnion} from '@horizon/server-utils'; const Collection = require('./collection').Collection; -const utils = require('../utils'); +const utils = require('./utils'); const assert = require('assert'); const version_field = '$hz_v$'; const metadata_version = [2, 0, 0]; -const create_collection = (r, db, name, conn) => +export const create_collection = (r, db, name, conn) => r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => r.branch( res('errors').ne(0), @@ -20,7 +20,7 @@ const create_collection = (r, db, name, conn) => ) ).run(conn); -const initialize_metadata = (r, db, conn) => +export const initialize_metadata = (r, db, conn) => r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) .then(() => Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => @@ -156,19 +156,16 @@ class ReliableInit extends Reliable { } } -class ReliableMetadata extends Reliable { - constructor(project_name, - reliable_conn, +export class ReliableMetadata extends Reliable { + constructor(server, auto_create_collection, - auto_create_index, - logger, - r) { + auto_create_index) { super(); - this.logger = logger; - this.r = r; + this.logger = server.logger; + this.r = server.r; - this._db = project_name; - this._reliable_conn = reliable_conn; + this._db = server.options.project_name; + this._reliable_conn = server.rdb_connection(); this._auto_create_collection = auto_create_collection; this._auto_create_index = auto_create_index; this._collections = new Map(); @@ -185,12 +182,11 @@ class ReliableMetadata extends Reliable { }, }); - this._collection_changefeed = new ReliableChangefeed( + this._collection_changefeed = server.makeReliableChangefeed( r.db(this._db) .table('hz_collections') .filter((row) => row('id').match('^hzp?_').not()) .changes({squash: false, includeInitial: true, includeTypes: true}), - reliable_conn, { onChange: (change) => { switch (change.type) { @@ -380,5 +376,3 @@ class ReliableMetadata extends Reliable { return this._reliable_conn; } } - -module.exports = {ReliableMetadata, create_collection, initialize_metadata}; diff --git a/plugins/collection/src/table.js b/plugins/collection/src/table.js index 99b9b3e1a..a2973750b 100644 --- a/plugins/collection/src/table.js +++ b/plugins/collection/src/table.js @@ -59,7 +59,7 @@ class Table { indexes.forEach((name) => { try { const old_index = this.indexes.get(name); - const new_index = new index.Index(name, this.table, conn); + const new_index = new index.Index(this.logger, name, this.table, conn); if (old_index) { // Steal any waiters from the old index new_index._waiters = old_index._waiters; @@ -85,7 +85,7 @@ class Table { const success = () => { // Create the Index object now so we don't try to create it again before the // feed notifies us of the index creation - const new_index = new index.Index(index_name, this.table, conn); + const new_index = new index.Index(this.logger, index_name, this.table, conn); // TODO: shouldn't this be done before we go async? this.indexes.set(index_name, new_index); return new_index.on_ready(done); diff --git a/server/src/utils.js b/plugins/collection/src/utils.js similarity index 100% rename from server/src/utils.js rename to plugins/collection/src/utils.js diff --git a/plugins/timeout/.babelrc b/plugins/timeout/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/timeout/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/server-utils/src/reliable.js b/server-utils/src/reliable.js new file mode 100644 index 000000000..c9f821699 --- /dev/null +++ b/server-utils/src/reliable.js @@ -0,0 +1,117 @@ +'use strict'; + +import * as r from 'rethinkdb'; + +const subs = Symbol('subs'); + +export class Reliable { + constructor(initialCbs) { + this[subs] = new Map(); + this.ready = false; + this.closed = false; + if (initialCbs) { + this.subscribe(initialCbs); + } + } + + numSubs() { + return this[subs].size; + } + + subscribe(cbs) { + if (this.closed) { + throw new Error('Cannot subscribe to a closed ReliableConn.'); + } + const subId = Symbol(); + this[subs].set(subId, { + cbs: cbs, + close: () => this[subs].delete(subId), + }); + if (this.ready && cbs.onReady) { + try { + cbs.onReady.apply(cbs, this.ready); + } catch (e) { + logger.error('Unexpected error in reliable callback, ' + + `event: subscribe onReady, error: ${e.stack}`); + } + } + return this[subs].get(subId); + } + + emit() { + if (!this.closed) { + this.emitInternal.apply(this, arguments); + } + } + + emitInternal(eventType, ...args) { + // TODO: consider checking to make sure we don't send two + // `onReady` or `onUnready`s in a row (or even just returning + // early if we would). + if (eventType === 'onReady') { + this.ready = args; + } else if (eventType === 'onUnready') { + this.ready = false; + } + this[subs].forEach((sub) => { + try { + const event = sub.cbs[eventType]; + if (event) { + event.apply(sub.cbs, args); + } + } catch (e) { + logger.error('Unexpected error in reliable callback, ' + + `event: ${eventType}, error: ${e.stack}`); + } + }); + } + + close(reason) { + this.closed = true; + if (this.ready) { + this.emitInternal('onUnready', new Error(`closed: ${reason}`)); + } + this[subs].clear(); // Just to make sure no subclasses do anything clever. + return Promise.resolve(); + } +} + +export class ReliableUnion extends Reliable { + constructor(reqs, cbs) { + super(cbs); + this.reqs = reqs; + this.subs = {}; + this.emitArg = {}; + this.readyNeeded = 0; + for (const k in reqs) { + this.subs[k] = reqs[k].subscribe({ + onReady: (...rest) => { + this.readyNeeded -= 1; + this.emitArg[k] = rest; + this.maybeEmit(); + }, + onUnready: (...rest) => { + this.readyNeeded += 1; + this.emitArg[k] = rest; + this.maybeEmit(); + }, + }); + this.readyNeeded += 1; + } + } + + maybeEmit() { + if (this.readyNeeded === 0 && !this.ready) { + this.emit('onReady', this.emitArg); + } else if (this.readyNeeded !== 0 && this.ready) { + this.emit('onUnready', this.emitArg); + } + } + + close(reason) { + for (const k in this.subs) { + this.subs[k].close(); + } + return super.close(reason); + } +} diff --git a/server/package.json b/server/package.json index eb72c1253..9ba987e23 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@horizon/client": "2.0.0-beta-5", + "@horizon/server-utils": "1.0.0", "bluebird": "^3.4.0", "cookie": "^0.2.3", "joi": "^8.0.4", diff --git a/server/src/reliable.js b/server/src/reliable.js index 7fd32e085..20912ed91 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -1,84 +1,9 @@ 'use strict'; -const logger = require('./logger'); +import * as r from 'rethinkdb'; +import {Reliable} from '@horizon/server-utils'; -const r = require('rethinkdb'); - -const subs = Symbol('subs'); - -class Reliable { - constructor(initialCbs) { - this[subs] = new Map(); - this.ready = false; - this.closed = false; - if (initialCbs) { - this.subscribe(initialCbs); - } - } - - numSubs() { - return this[subs].size; - } - - subscribe(cbs) { - if (this.closed) { - throw new Error('Cannot subscribe to a closed ReliableConn.'); - } - const subId = Symbol(); - this[subs].set(subId, { - cbs: cbs, - close: () => this[subs].delete(subId), - }); - if (this.ready && cbs.onReady) { - try { - cbs.onReady.apply(cbs, this.ready); - } catch (e) { - logger.error('Unexpected error in reliable callback, ' + - `event: subscribe onReady, error: ${e.stack}`); - } - } - return this[subs].get(subId); - } - - emit() { - if (!this.closed) { - this.emitInternal.apply(this, arguments); - } - } - - emitInternal(eventType, ...args) { - // TODO: consider checking to make sure we don't send two - // `onReady` or `onUnready`s in a row (or even just returning - // early if we would). - if (eventType === 'onReady') { - this.ready = args; - } else if (eventType === 'onUnready') { - this.ready = false; - } - this[subs].forEach((sub) => { - try { - const event = sub.cbs[eventType]; - if (event) { - event.apply(sub.cbs, args); - } - } catch (e) { - logger.error('Unexpected error in reliable callback, ' + - `event: ${eventType}, error: ${e.stack}`); - } - }); - } - - close(reason) { - this.closed = true; - if (this.ready) { - this.emitInternal('onUnready', new Error(`closed: ${reason}`)); - } - this[subs].clear(); // Just to make sure no subclasses do anything clever. - return Promise.resolve(); - } -} - -class ReliableConn extends Reliable { +export class ReliableConn extends Reliable { constructor(connOpts) { super(); this.connOpts = connOpts; @@ -103,7 +28,8 @@ class ReliableConn extends Reliable { } }).catch((e) => { if (this.conn) { - logger.error(`Error in ReliableConnection ${JSON.stringify(this.connOpts)}: ${e.stack}`); + logger.error( + `Error in ReliableConnection ${JSON.stringify(this.connOpts)}: ${e.stack}`); } if (!this.closed) { setTimeout(() => this.connect(), 1000); @@ -131,7 +57,7 @@ class ReliableConn extends Reliable { } } -class ReliableChangefeed extends Reliable { +export class ReliableChangefeed extends Reliable { constructor(reql, reliableConn, cbs) { super(cbs); this.reql = reql; @@ -191,50 +117,3 @@ class ReliableChangefeed extends Reliable { return retProm; } } - -class ReliableUnion extends Reliable { - constructor(reqs, cbs) { - super(cbs); - this.reqs = reqs; - this.subs = {}; - this.emitArg = {}; - this.readyNeeded = 0; - for (const k in reqs) { - this.subs[k] = reqs[k].subscribe({ - onReady: (...rest) => { - this.readyNeeded -= 1; - this.emitArg[k] = rest; - this.maybeEmit(); - }, - onUnready: (...rest) => { - this.readyNeeded += 1; - this.emitArg[k] = rest; - this.maybeEmit(); - }, - }); - this.readyNeeded += 1; - } - } - - maybeEmit() { - if (this.readyNeeded === 0 && !this.ready) { - this.emit('onReady', this.emitArg); - } else if (this.readyNeeded !== 0 && this.ready) { - this.emit('onUnready', this.emitArg); - } - } - - close(reason) { - for (const k in this.subs) { - this.subs[k].close(); - } - return super.close(reason); - } -} - -module.exports = { - Reliable, - ReliableConn, - ReliableChangefeed, - ReliableUnion, -}; diff --git a/server/src/schema/server_options.js b/server/src/schema/server_options.js index 8844ca20d..5ba26e098 100644 --- a/server/src/schema/server_options.js +++ b/server/src/schema/server_options.js @@ -7,15 +7,9 @@ const server = Joi.object({ rdb_host: Joi.string().hostname().default('localhost'), rdb_port: Joi.number().greater(0).less(65536).default(28015), - auto_create_collection: Joi.boolean().default(false), - auto_create_index: Joi.boolean().default(false), - - permissions: Joi.boolean().default(true), - path: Joi.string().default('/horizon'), auth: Joi.object().default({ }), - access_control_allow_origin: Joi.string().allow('').default(''), rdb_user: Joi.string().allow(null), rdb_password: Joi.string().allow(null), diff --git a/server/src/server.js b/server/src/server.js index ddc7faeb3..97988265f 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -116,6 +116,10 @@ class Server extends EventEmitter { } } + makeReliableChangefeed(reql, ...args) { + return new ReliableChangefeed(reql, this._reliableConn, ...args); + } + auth() { return this._auth; } diff --git a/test/serve.js b/test/serve.js index 0cf6e98a0..ed652e84f 100755 --- a/test/serve.js +++ b/test/serve.js @@ -10,7 +10,7 @@ const PluginRouter = require('../plugin_router'); const permit_all = require('../plugins/permit_all'); const reads = require('../plugins/reads'); const writes = require('../plugins/writes'); -const collection = require('../plugins/collection'); +const collection = require('../plugins/collection').default; const timeout = require('../plugins/timeout'); // Utilities provided by the CLI library @@ -175,10 +175,7 @@ new Promise((resolve) => { horizon.logger.level = 'debug'; const horizon_server = new horizon.Server(http_servers, { - auto_create_collection: true, - auto_create_index: true, rdb_port: server.driver_port, - permissions: parse_yes_no_option(options.permissions), project_name: 'hz_test', auth: { allow_unauthenticated: true, @@ -190,28 +187,36 @@ new Promise((resolve) => { const plugins = new PluginRouter(horizon_server); plugins.add(permit_all()); - plugins.add(collection()); + plugins.add(collection({ + auto_create_collection: true, + auto_create_index: true, + })); plugins.add(timeout()); plugins.add(reads()); plugins.add(writes()); - horizon_server.set_middleware(plugins.hzMiddleware()); + plugins.once('ready', () => { + console.log('READY OMGZZZZ'); + horizon_server.set_middleware(plugins.hzMiddleware()); + + // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server + http_servers.forEach((serv, i) => { + const extant_listeners = serv.listeners('request').slice(0); + serv.removeAllListeners('request'); + serv.on('request', (req, res) => { + const req_path = url.parse(req.url).pathname; + if (req_path === '/horizon/horizon.js' || + req_path === '/horizon/horizon.js.map') { + serve_file(path.resolve(test_dist_dir, req_path.replace('/horizon/', '')), + res); + } else { + extant_listeners.forEach((l) => l.call(serv, req, res)); + } + }); - // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server - http_servers.forEach((serv, i) => { - const extant_listeners = serv.listeners('request').slice(0); - serv.removeAllListeners('request'); - serv.on('request', (req, res) => { - const req_path = url.parse(req.url).pathname; - if (req_path === '/horizon/horizon.js' || req_path === '/horizon/horizon.js.map') { - serve_file(path.resolve(test_dist_dir, req_path.replace('/horizon/', '')), res); - } else { - extant_listeners.forEach((l) => l.call(serv, req, res)); - } + serv.listen(options.port, options.bind[i], + () => console.log(`HTTP server listening on ${options.bind[i]}:${options.port}.`)); }); - - serv.listen(options.port, options.bind[i], - () => console.log(`HTTP server listening on ${options.bind[i]}:${options.port}.`)); }); }).catch((err) => { console.log(`Error when starting server:\n${err.stack}`); From c99fd739885855b321119ecff6d689f2523c06c7 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Thu, 1 Sep 2016 01:22:41 +0000 Subject: [PATCH 44/86] added stuff --- server-utils/.babelrc | 3 +++ server-utils/.eslintrc.js | 17 +++++++++++++++++ server-utils/package.json | 35 +++++++++++++++++++++++++++++++++++ server-utils/src/utils.js | 3 +++ 4 files changed, 58 insertions(+) create mode 100644 server-utils/.babelrc create mode 100644 server-utils/.eslintrc.js create mode 100644 server-utils/package.json create mode 100644 server-utils/src/utils.js diff --git a/server-utils/.babelrc b/server-utils/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/server-utils/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/server-utils/.eslintrc.js b/server-utils/.eslintrc.js new file mode 100644 index 000000000..98faeb27b --- /dev/null +++ b/server-utils/.eslintrc.js @@ -0,0 +1,17 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "camelcase": [ ERROR ], + "max-len": [ ERROR, 130 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/server-utils/package.json b/server-utils/package.json new file mode 100644 index 000000000..aa8a75a50 --- /dev/null +++ b/server-utils/package.json @@ -0,0 +1,35 @@ +{ + "name": "@horizon/server-utils", + "version": "1.0.0", + "description": "Utilities for the Horizon server and its plugins.", + "main": "dist/utils.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha dist/test --timeout 10000", + "coverage": "istanbul cover _mocha test/test.js", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "rethinkdb": "^2.1.1" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/server-utils/src/utils.js b/server-utils/src/utils.js new file mode 100644 index 000000000..c5c79e07e --- /dev/null +++ b/server-utils/src/utils.js @@ -0,0 +1,3 @@ +'use strict'; + +export {Reliable, ReliableUnion} from './reliable'; From 102fdc0351d0da69d4e26086f986243c3cfc4b0a Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 31 Aug 2016 19:56:27 -0700 Subject: [PATCH 45/86] fixing bugs in testing --- babelify.sh | 2 +- plugin_router/src/index.js | 14 ++++---- plugin_router/src/request.js | 5 ++- plugins/collection/src/collection.js | 2 +- plugins/collection/src/main.js | 8 +++-- plugins/collection/src/metadata.js | 52 +++++++++++++--------------- plugins/permit_all/src/index.js | 2 +- plugins/reads/src/fetch.js | 2 +- plugins/reads/src/watch.js | 2 +- plugins/writes/src/insert.js | 2 +- plugins/writes/src/remove.js | 2 +- plugins/writes/src/replace.js | 2 +- plugins/writes/src/store.js | 2 +- plugins/writes/src/update.js | 2 +- plugins/writes/src/upsert.js | 2 +- server-utils/src/reliable.js | 10 +++--- server/src/client.js | 2 +- server/src/reliable.js | 3 +- test/serve.js | 20 ++++++----- 19 files changed, 73 insertions(+), 63 deletions(-) mode change 100644 => 100755 babelify.sh diff --git a/babelify.sh b/babelify.sh old mode 100644 new mode 100755 index 07db60d20..30bee422f --- a/babelify.sh +++ b/babelify.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash dirs="server server-utils plugins plugin_router" for path in `find $dirs -name .babelrc | grep -v node_modules`; do diff --git a/plugin_router/src/index.js b/plugin_router/src/index.js index 9400779c3..0ab42be3c 100644 --- a/plugin_router/src/index.js +++ b/plugin_router/src/index.js @@ -17,6 +17,7 @@ class PluginRouter extends EventEmitter { noteReady(plugin) { if (!this.readyPlugins.has(plugin)) { + console.log(`noteReady(${plugin}): ${this.readyPlugins.size}/${this.plugins.size}`); this.readyPlugins.add(plugin); this.emit('pluginReady', plugin, this); if (this.readyPlugins.size === this.plugins.size) { @@ -27,6 +28,7 @@ class PluginRouter extends EventEmitter { noteUnready(plugin) { if (this.readyPlugins.has(plugin)) { + console.log(`noteUnready(${plugin}): ${this.readyPlugins.size}/${this.plugins.size}`); this.readyPlugins.delete(plugin); this.emit('pluginUnready', plugin, this); if (this.readyPlugins.size === this.plugins.size - 1) { @@ -40,9 +42,12 @@ class PluginRouter extends EventEmitter { return Promise.reject( new Error(`Plugin conflict: "${plugin.name}" already present.`)); } - const activePlugin = Promise.resolve(this.server).then((server) => { + // Placeholder so we don't say we're ready too soon + this.plugins.set(plugin.name, null); + this.plugins.set(plugin.name, Promise.resolve(this.server).then((server) => { this.emit('unready', this); if (plugin.activate.length > 1) { + console.log(`activating ${plugin.name} with ready callbacks`); return plugin.activate( server, () => this.noteReady(plugin.name), @@ -53,8 +58,7 @@ class PluginRouter extends EventEmitter { return x; }); } - }); - this.plugins.set(plugin.name, activePlugin.then((active) => { + }).then((active) => { if (this.httpRoutes[plugin.name]) { throw new Error(`Plugin conflict: "${plugin.name}" already present.`); } @@ -71,10 +75,6 @@ class PluginRouter extends EventEmitter { this.methods[m] = active.methods[m]; this._requirementsOrdering = null; } - }).catch((err) => { - this.server.logger.error(`Error when adding plugin ${plugin.name}: ${err}`); - this.server.logger.debug(`${err.stack}`); - throw err; })); return this.plugins.get(plugin.name); } diff --git a/plugin_router/src/request.js b/plugin_router/src/request.js index bf7f075c4..f6c2ff1f7 100644 --- a/plugin_router/src/request.js +++ b/plugin_router/src/request.js @@ -1,12 +1,15 @@ 'use strict'; +const assert = require('assert'); + class Request { constructor(request, currentMethod) { this.request_id = request.request_id; this.options = request.options; this.clientCtx = request.clientCtx; - this._parameters = request.parameters; + this._parameters = request._parameters; this._currentMethod = currentMethod; + Object.freeze(this); } getParameter(methodName) { diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index cb47c4d20..501c32134 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -66,7 +66,7 @@ class Collection { _get_table() { if (this._tables.size === 0) { - throw new error.CollectionNotReady(this); + throw new Error(`Collection ${this.name} is not ready.`); } return this._tables.values().next().value; } diff --git a/plugins/collection/src/main.js b/plugins/collection/src/main.js index 48db8f792..1e26e0100 100644 --- a/plugins/collection/src/main.js +++ b/plugins/collection/src/main.js @@ -11,6 +11,7 @@ function collection(server, metadata) { next(new Error('First argument to "collection" must be a string.')); } else { metadata.collection(args[0]).then((collection) => { + // RSI: pick up here trucks here reads aren't getting this? req.setParameter(collection); next(); }).catch(next); @@ -20,7 +21,7 @@ function collection(server, metadata) { export default function(raw_config) { return { - name: (raw_config && raw_config.name) || 'permissions', + name: (raw_config && raw_config.name) || 'collection', // RSI: make sure we check the arity and become ready if people // don't take the callbacks. activate: (server, onReady, onUnready) => { @@ -28,7 +29,10 @@ export default function(raw_config) { server, raw_config.auto_create_collection, raw_config.auto_create_index); - metadata.subscribe({onReady, onUnready}); + metadata.subscribe({onReady: () => { + console.log('metadata ready'); + onReady(); + }, onUnready}); return { methods: { collection: { diff --git a/plugins/collection/src/metadata.js b/plugins/collection/src/metadata.js index d4e6d4a23..5eee3dd42 100644 --- a/plugins/collection/src/metadata.js +++ b/plugins/collection/src/metadata.js @@ -44,14 +44,15 @@ class StaleAttemptError extends Error { } // RSI: fix all this shit. class ReliableInit extends Reliable { - constructor(db, reliable_conn, auto_create_collection) { + constructor(r, logger, db, reliable_conn, auto_create_collection) { super(); + this.logger = logger; this._db = db; this._auto_create_collection = auto_create_collection; this._conn_subs = reliable_conn.subscribe({ onReady: (conn) => { this.current_attempt = Symbol(); - this.do_init(conn, this.current_attempt); + this.do_init(r, conn, this.current_attempt); }, onUnready: () => { this.current_attempt = null; @@ -68,15 +69,15 @@ class ReliableInit extends Reliable { } } - do_init(conn, attempt) { + do_init(r, conn, attempt) { Promise.resolve().then(() => { this.check_attempt(attempt); - logger.debug('checking rethinkdb version'); + this.logger.debug('checking rethinkdb version'); const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version'); return q.run(conn).then((res) => utils.rethinkdb_version_check(res)); }).then(() => { this.check_attempt(attempt); - logger.debug('checking for old metadata version'); + this.logger.debug('checking for old metadata version'); const old_metadata_db = `${this._db}_internal`; return r.dbList().contains(old_metadata_db).run(conn).then((has_old_db) => { if (has_old_db) { @@ -87,9 +88,9 @@ class ReliableInit extends Reliable { }); }).then(() => { this.check_attempt(attempt); - logger.debug('checking for internal tables'); + this.logger.debug('checking for internal tables'); if (this._auto_create_collection) { - return initialize_metadata(this.r, this._db, conn); + return initialize_metadata(r, this._db, conn); } else { return r.dbList().contains(this._db).run(conn).then((has_db) => { if (!has_db) { @@ -101,12 +102,12 @@ class ReliableInit extends Reliable { } }).then(() => { this.check_attempt(attempt); - logger.debug('waiting for internal tables'); + this.logger.debug('waiting for internal tables'); return r.expr(['hz_collections', 'hz_users_auth', 'hz_groups', 'users']) .forEach((table) => r.db(this._db).table(table).wait({timeout: 30})).run(conn); }).then(() => { this.check_attempt(attempt); - logger.debug('adding admin user'); + this.logger.debug('adding admin user'); return Promise.all([ r.db(this._db).table('users').get('admin') .replace((old_row) => @@ -139,12 +140,12 @@ class ReliableInit extends Reliable { ]); }).then(() => { this.check_attempt(attempt); - logger.debug('metadata sync complete'); + this.logger.debug('metadata sync complete'); this.emit('onReady'); }).catch((err) => { if (!(err instanceof StaleAttemptError)) { - logger.debug(`Metadata initialization failed: ${err}`); - setTimeout(() => { this.do_init(conn, attempt); }, 1000); + this.logger.debug(`Metadata initialization failed: ${err.stack}`); + setTimeout(() => { this.do_init(r, conn, attempt); }, 1000); } }); } @@ -171,7 +172,7 @@ export class ReliableMetadata extends Reliable { this._collections = new Map(); this._reliable_init = new ReliableInit( - this._db, reliable_conn, auto_create_collection); + this.r, this.logger, this._db, this._reliable_conn, auto_create_collection); this._conn_subscription = this._reliable_conn.subscribe({ onReady: (conn) => { @@ -183,7 +184,7 @@ export class ReliableMetadata extends Reliable { }); this._collection_changefeed = server.makeReliableChangefeed( - r.db(this._db) + this.r.db(this._db) .table('hz_collections') .filter((row) => row('id').match('^hzp?_').not()) .changes({squash: false, includeInitial: true, includeTypes: true}), @@ -198,7 +199,7 @@ export class ReliableMetadata extends Reliable { let collection = this._collections.get(collection_name); if (!collection) { collection = new Collection( - this._db, collection_name, this.logger, this.r); + this._db, collection_name, this._reliable_conn, this.logger, this.r); this._collections.set(collection_name, collection); } collection._register(); @@ -225,19 +226,17 @@ export class ReliableMetadata extends Reliable { }, }); - - this._index_changefeed = new ReliableChangefeed( - r.db('rethinkdb') + this._index_changefeed = server.makeReliableChangefeed( + this.r.db('rethinkdb') .table('table_config') - .filter((row) => r.and(row('db').eq(this._db), - row('name').match('^hzp?_').not())) + .filter((row) => this.r.and(row('db').eq(this._db), + row('name').match('^hzp?_').not())) .map((row) => ({ id: row('id'), name: row('name'), indexes: row('indexes').filter((idx) => idx.match('^hz_')), })) .changes({squash: true, includeInitial: true, includeTypes: true}), - reliable_conn, { onChange: (change) => { if (!this._connection) { return; } @@ -252,7 +251,7 @@ export class ReliableMetadata extends Reliable { let collection = this._collections.get(collection_name); if (!collection) { collection = new Collection( - this._db, collection_name, this.logger, this.r); + this._db, collection_name, this._reliable_conn, this.logger, this.r); this._collections.set(collection_name, collection); } collection._update_table( @@ -338,6 +337,7 @@ export class ReliableMetadata extends Reliable { } create_collection(name) { + let collection; return Promise.resolve().then(() => { if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { throw new Error(`Collection "${name}" is reserved for internal use ` + @@ -348,7 +348,7 @@ export class ReliableMetadata extends Reliable { throw new Error(`Collection "${name}" already exists.`); } - const collection = new Collection(this._db, name, this.logger, this.r); + collection = new Collection(this._db, name, this._reliable_conn, this.logger, this.r); this._collections.set(name, collection); return create_collection(this.r, this._db, name, this._reliable_conn.connection()); @@ -364,15 +364,11 @@ export class ReliableMetadata extends Reliable { } })); }).catch((err) => { - if (collection._is_safe_to_remove()) { + if (collection && collection._is_safe_to_remove()) { this._collections.delete(name); collection.close(); } throw err; }); } - - reliable_connection() { - return this._reliable_conn; - } } diff --git a/plugins/permit_all/src/index.js b/plugins/permit_all/src/index.js index 7e3723cb9..cce6a7de1 100644 --- a/plugins/permit_all/src/index.js +++ b/plugins/permit_all/src/index.js @@ -8,7 +8,7 @@ module.exports = (raw_config) => ({ hz_permissions: { type: 'preReq', handler: (req, res, next) => { - req.validate = () => null; + req.setParameter(() => null); next(); }, }, diff --git a/plugins/reads/src/fetch.js b/plugins/reads/src/fetch.js index 056c1e5f1..8b73ab930 100644 --- a/plugins/reads/src/fetch.js +++ b/plugins/reads/src/fetch.js @@ -4,7 +4,7 @@ const common = require('./common'); module.exports = (server) => (req, res, next) => { const args = req.options.fetch; - const permissions = req.getParameter('permissions'); + const permissions = req.getParameter('hz_permissions'); const conn = server.rdb_connection().connection(); if (args.length !== 0) { diff --git a/plugins/reads/src/watch.js b/plugins/reads/src/watch.js index 96e4cfe07..0efc067e8 100644 --- a/plugins/reads/src/watch.js +++ b/plugins/reads/src/watch.js @@ -4,7 +4,7 @@ const common = require('./common'); module.exports = (server) => (req, res, next) => { const args = req.options.watch; - const permissions = req.getParameter('permissions'); + const permissions = req.getParameter('hz_permissions'); const conn = server.rdb_connection().connection(); if (args.length !== 0) { diff --git a/plugins/writes/src/insert.js b/plugins/writes/src/insert.js index b5e3a4bb7..09003c609 100644 --- a/plugins/writes/src/insert.js +++ b/plugins/writes/src/insert.js @@ -7,7 +7,7 @@ module.exports = (server) => (request, response, next) => { const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); - const permissions = request.getParameter('permissions'); + const permissions = request.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); diff --git a/plugins/writes/src/remove.js b/plugins/writes/src/remove.js index edb8e2e5c..bad3db16b 100644 --- a/plugins/writes/src/remove.js +++ b/plugins/writes/src/remove.js @@ -8,7 +8,7 @@ module.exports = (server) => (request, response, next) => { const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); - const permissions = request.getParameter('permissions'); + const permissions = request.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); diff --git a/plugins/writes/src/replace.js b/plugins/writes/src/replace.js index 07bb3a555..11019218e 100644 --- a/plugins/writes/src/replace.js +++ b/plugins/writes/src/replace.js @@ -8,7 +8,7 @@ module.exports = (server) => (request, response, next) => { const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); - const permissions = request.getParameter('permissions'); + const permissions = request.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); diff --git a/plugins/writes/src/store.js b/plugins/writes/src/store.js index 63b89ab12..90f93569a 100644 --- a/plugins/writes/src/store.js +++ b/plugins/writes/src/store.js @@ -8,7 +8,7 @@ module.exports = (server) => (request, response, next) => { const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); - const permissions = request.getParameter('permissions'); + const permissions = request.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); diff --git a/plugins/writes/src/update.js b/plugins/writes/src/update.js index 6da8ceb2e..842e882e6 100644 --- a/plugins/writes/src/update.js +++ b/plugins/writes/src/update.js @@ -8,7 +8,7 @@ module.exports = (server) => (request, response, next) => { const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); - const permissions = request.getParameter('permissions'); + const permissions = request.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); diff --git a/plugins/writes/src/upsert.js b/plugins/writes/src/upsert.js index c8462ce27..f0ce9e4b1 100644 --- a/plugins/writes/src/upsert.js +++ b/plugins/writes/src/upsert.js @@ -8,7 +8,7 @@ module.exports = (server) => (request, response, next) => { const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); - const permissions = request.getParameter('permissions'); + const permissions = request.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); diff --git a/server-utils/src/reliable.js b/server-utils/src/reliable.js index c9f821699..f7661de41 100644 --- a/server-utils/src/reliable.js +++ b/server-utils/src/reliable.js @@ -31,8 +31,9 @@ export class Reliable { try { cbs.onReady.apply(cbs, this.ready); } catch (e) { - logger.error('Unexpected error in reliable callback, ' + - `event: subscribe onReady, error: ${e.stack}`); + // RSI: use logging facilities + console.error('Unexpected error in reliable callback, ' + + `event: subscribe onReady, error: ${e.stack}`); } } return this[subs].get(subId); @@ -60,8 +61,9 @@ export class Reliable { event.apply(sub.cbs, args); } } catch (e) { - logger.error('Unexpected error in reliable callback, ' + - `event: ${eventType}, error: ${e.stack}`); + // RSI: use logging facilities + console.error('Unexpected error in reliable callback, ' + + `event: ${eventType}, error: ${e.stack}`); } }); } diff --git a/server/src/client.js b/server/src/client.js index b417adb18..0199726be 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -80,7 +80,7 @@ class ClientConnection { if (request.request_id === undefined) { // This is pretty much an unrecoverable protocol error, so close the connection - this.close({reqId, error: `Protocol error: ${err}`, error_code: 0}); + this.close({reqId, error: `Protocol error: ${err_str}`, error_code: 0}); } else { this.send_error(reqId, new Error(err_str)); } diff --git a/server/src/reliable.js b/server/src/reliable.js index 20912ed91..22ba7e6ad 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -2,6 +2,7 @@ import * as r from 'rethinkdb'; import {Reliable} from '@horizon/server-utils'; +const logger = require('./logger'); export class ReliableConn extends Reliable { constructor(connOpts) { @@ -91,7 +92,7 @@ export class ReliableChangefeed extends Reliable { throw new Error(`cursor closed unexpectedly: ${res}`); }); }).catch((e) => { - logger.debug(`Changefeed error (${this.reql}): ${e}`); + logger.debug(`Changefeed error (${this.reql}): ${e.stack}`); if (this.ready) { this.emit('onUnready', e); } diff --git a/test/serve.js b/test/serve.js index ed652e84f..a05519493 100755 --- a/test/serve.js +++ b/test/serve.js @@ -186,14 +186,18 @@ new Promise((resolve) => { console.log('starting http servers'); const plugins = new PluginRouter(horizon_server); - plugins.add(permit_all()); - plugins.add(collection({ - auto_create_collection: true, - auto_create_index: true, - })); - plugins.add(timeout()); - plugins.add(reads()); - plugins.add(writes()); + Promise.all([ + plugins.add(permit_all()), + plugins.add(collection({ + auto_create_collection: true, + auto_create_index: true, + })), + plugins.add(timeout()), + plugins.add(reads()), + plugins.add(writes()), + ]).catch((err) => + console.log(`Plugin initialization failed: ${err.stack}`) + ); plugins.once('ready', () => { console.log('READY OMGZZZZ'); From b281558b9ec9e5f5ca8b7a75228cf397d680f58e Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 1 Sep 2016 20:14:52 -0700 Subject: [PATCH 46/86] reworking plugin dependencies --- plugins/collection/package.json | 5 +- plugins/collection/src/collection.js | 8 +- plugins/collection/src/index.js | 4 +- plugins/collection/src/metadata.js | 55 ++++++------ plugins/collection/src/table.js | 18 ++-- plugins/permissions/package.json | 4 + plugins/permissions/src/permissions.js | 13 ++- plugins/permit_all/package.json | 3 + plugins/reads/package.json | 4 + plugins/reads/src/common.js | 4 +- plugins/reads/src/fetch.js | 2 +- plugins/reads/src/watch.js | 2 +- plugins/timeout/package.json | 3 + plugins/writes/package.json | 4 + plugins/writes/src/common.js | 4 +- plugins/writes/src/insert.js | 5 +- plugins/writes/src/remove.js | 3 +- plugins/writes/src/replace.js | 5 +- plugins/writes/src/store.js | 10 +-- plugins/writes/src/update.js | 6 +- plugins/writes/src/upsert.js | 10 +-- server-utils/.babelrc | 3 - server-utils/.eslintrc.js | 17 ---- server-utils/package.json | 35 -------- server-utils/src/reliable.js | 119 ------------------------- server-utils/src/utils.js | 3 - server/src/index.js | 15 ++-- server/src/reliable.js | 117 +++++++++++++++++++++++- 28 files changed, 220 insertions(+), 261 deletions(-) delete mode 100644 server-utils/.babelrc delete mode 100644 server-utils/.eslintrc.js delete mode 100644 server-utils/package.json delete mode 100644 server-utils/src/reliable.js delete mode 100644 server-utils/src/utils.js diff --git a/plugins/collection/package.json b/plugins/collection/package.json index d199d9323..21466cbba 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -22,7 +22,10 @@ "node": ">=4.0.0" }, "dependencies": { - "@horizon/server-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" }, "devDependencies": { "eslint": "^2.3.0", diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 501c32134..8b2d98ef0 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -1,13 +1,13 @@ 'use strict'; +const {r, logger} = require('@horizon/server'); + const Table = require('./table').Table; class Collection { - constructor(db, name, reliableConn, logger, r) { + constructor(db, name, reliableConn) { this.name = name; this.reliableConn = reliableConn; - this.logger = logger; - this.r = r; this.table = r.db(db).table(name); // This is the ReQL Table object this._tables = new Map(); // A Map of Horizon Table objects this._registered = false; // Whether the `hz_collections` table says this collection exists @@ -28,7 +28,7 @@ class Collection { let table = this._tables.get(table_id); if (indexes) { if (!table) { - table = new Table(this.table, conn, this.logger, this.r); + table = new Table(this.table, conn); this._tables.set(table_id, table); } table.update_indexes(indexes, conn); diff --git a/plugins/collection/src/index.js b/plugins/collection/src/index.js index 5308cc3c9..f2a052190 100644 --- a/plugins/collection/src/index.js +++ b/plugins/collection/src/index.js @@ -2,6 +2,8 @@ const assert = require('assert'); +const {logger} = require('@horizon/server'); + // Index names are of the format "hz_[_]" where may be // omitted or "multi_" or "geo" (at the moment). is a JSON array // specifying which fields are indexed in which order. The value at each index @@ -101,7 +103,7 @@ const compare_fields = (a, b) => { }; class Index { - constructor(logger, name, table, conn) { + constructor(name, table, conn) { logger.debug(`${table} index registered: ${name}`); const info = name_to_info(name); this.name = name; diff --git a/plugins/collection/src/metadata.js b/plugins/collection/src/metadata.js index 5eee3dd42..db3602a83 100644 --- a/plugins/collection/src/metadata.js +++ b/plugins/collection/src/metadata.js @@ -1,6 +1,6 @@ 'use strict'; -import {Reliable, ReliableUnion} from '@horizon/server-utils'; +import {r, logger, Reliable, ReliableUnion} from '@horizon/server'; const Collection = require('./collection').Collection; const utils = require('./utils'); @@ -9,7 +9,7 @@ const assert = require('assert'); const version_field = '$hz_v$'; const metadata_version = [2, 0, 0]; -export const create_collection = (r, db, name, conn) => +export const create_collection = (db, name, conn) => r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => r.branch( res('errors').ne(0), @@ -20,7 +20,7 @@ export const create_collection = (r, db, name, conn) => ) ).run(conn); -export const initialize_metadata = (r, db, conn) => +export const initialize_metadata = (db, conn) => r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) .then(() => Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => @@ -44,15 +44,14 @@ class StaleAttemptError extends Error { } // RSI: fix all this shit. class ReliableInit extends Reliable { - constructor(r, logger, db, reliable_conn, auto_create_collection) { + constructor(db, reliable_conn, auto_create_collection) { super(); - this.logger = logger; this._db = db; this._auto_create_collection = auto_create_collection; this._conn_subs = reliable_conn.subscribe({ onReady: (conn) => { this.current_attempt = Symbol(); - this.do_init(r, conn, this.current_attempt); + this.do_init(conn, this.current_attempt); }, onUnready: () => { this.current_attempt = null; @@ -69,15 +68,15 @@ class ReliableInit extends Reliable { } } - do_init(r, conn, attempt) { + do_init(conn, attempt) { Promise.resolve().then(() => { this.check_attempt(attempt); - this.logger.debug('checking rethinkdb version'); + logger.debug('checking rethinkdb version'); const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version'); return q.run(conn).then((res) => utils.rethinkdb_version_check(res)); }).then(() => { this.check_attempt(attempt); - this.logger.debug('checking for old metadata version'); + logger.debug('checking for old metadata version'); const old_metadata_db = `${this._db}_internal`; return r.dbList().contains(old_metadata_db).run(conn).then((has_old_db) => { if (has_old_db) { @@ -88,9 +87,9 @@ class ReliableInit extends Reliable { }); }).then(() => { this.check_attempt(attempt); - this.logger.debug('checking for internal tables'); + logger.debug('checking for internal tables'); if (this._auto_create_collection) { - return initialize_metadata(r, this._db, conn); + return initialize_metadata(this._db, conn); } else { return r.dbList().contains(this._db).run(conn).then((has_db) => { if (!has_db) { @@ -102,12 +101,12 @@ class ReliableInit extends Reliable { } }).then(() => { this.check_attempt(attempt); - this.logger.debug('waiting for internal tables'); + logger.debug('waiting for internal tables'); return r.expr(['hz_collections', 'hz_users_auth', 'hz_groups', 'users']) .forEach((table) => r.db(this._db).table(table).wait({timeout: 30})).run(conn); }).then(() => { this.check_attempt(attempt); - this.logger.debug('adding admin user'); + logger.debug('adding admin user'); return Promise.all([ r.db(this._db).table('users').get('admin') .replace((old_row) => @@ -140,12 +139,12 @@ class ReliableInit extends Reliable { ]); }).then(() => { this.check_attempt(attempt); - this.logger.debug('metadata sync complete'); + logger.debug('metadata sync complete'); this.emit('onReady'); }).catch((err) => { if (!(err instanceof StaleAttemptError)) { - this.logger.debug(`Metadata initialization failed: ${err.stack}`); - setTimeout(() => { this.do_init(r, conn, attempt); }, 1000); + logger.debug(`Metadata initialization failed: ${err.stack}`); + setTimeout(() => { this.do_init(conn, attempt); }, 1000); } }); } @@ -162,9 +161,6 @@ export class ReliableMetadata extends Reliable { auto_create_collection, auto_create_index) { super(); - this.logger = server.logger; - this.r = server.r; - this._db = server.options.project_name; this._reliable_conn = server.rdb_connection(); this._auto_create_collection = auto_create_collection; @@ -172,7 +168,7 @@ export class ReliableMetadata extends Reliable { this._collections = new Map(); this._reliable_init = new ReliableInit( - this.r, this.logger, this._db, this._reliable_conn, auto_create_collection); + this._db, this._reliable_conn, auto_create_collection); this._conn_subscription = this._reliable_conn.subscribe({ onReady: (conn) => { @@ -183,8 +179,9 @@ export class ReliableMetadata extends Reliable { }, }); + // RSI: stop these from running until after ReliableInit? this._collection_changefeed = server.makeReliableChangefeed( - this.r.db(this._db) + r.db(this._db) .table('hz_collections') .filter((row) => row('id').match('^hzp?_').not()) .changes({squash: false, includeInitial: true, includeTypes: true}), @@ -199,7 +196,7 @@ export class ReliableMetadata extends Reliable { let collection = this._collections.get(collection_name); if (!collection) { collection = new Collection( - this._db, collection_name, this._reliable_conn, this.logger, this.r); + this._db, collection_name, this._reliable_conn); this._collections.set(collection_name, collection); } collection._register(); @@ -227,10 +224,10 @@ export class ReliableMetadata extends Reliable { }); this._index_changefeed = server.makeReliableChangefeed( - this.r.db('rethinkdb') + r.db('rethinkdb') .table('table_config') - .filter((row) => this.r.and(row('db').eq(this._db), - row('name').match('^hzp?_').not())) + .filter((row) => r.and(row('db').eq(this._db), + row('name').match('^hzp?_').not())) .map((row) => ({ id: row('id'), name: row('name'), @@ -251,7 +248,7 @@ export class ReliableMetadata extends Reliable { let collection = this._collections.get(collection_name); if (!collection) { collection = new Collection( - this._db, collection_name, this._reliable_conn, this.logger, this.r); + this._db, collection_name, this._reliable_conn); this._collections.set(collection_name, collection); } collection._update_table( @@ -348,13 +345,13 @@ export class ReliableMetadata extends Reliable { throw new Error(`Collection "${name}" already exists.`); } - collection = new Collection(this._db, name, this._reliable_conn, this.logger, this.r); + collection = new Collection(this._db, name, this._reliable_conn); this._collections.set(name, collection); - return create_collection(this.r, this._db, name, this._reliable_conn.connection()); + return create_collection(this._db, name, this._reliable_conn.connection()); }).then((res) => { assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); - this.logger.warn(`Collection created: "${name}"`); + logger.warn(`Collection created: "${name}"`); return new Promise((resolve, reject) => collection._on_ready((maybeErr) => { if (maybeErr instanceof Error) { diff --git a/plugins/collection/src/table.js b/plugins/collection/src/table.js index a2973750b..15530cbd1 100644 --- a/plugins/collection/src/table.js +++ b/plugins/collection/src/table.js @@ -3,11 +3,11 @@ const index = require('./index'); const assert = require('assert'); +const {r, logger} = require('@horizon/server'); + class Table { - constructor(reql_table, conn, logger, r) { + constructor(reql_table, conn) { this.table = reql_table; - this.logger = logger; - this.r = r; this.indexes = new Map(); this._waiters = []; @@ -50,7 +50,7 @@ class Table { } update_indexes(indexes, conn) { - this.logger.debug(`${this.table} indexes changed, reevaluating`); + logger.debug(`${this.table} indexes changed, reevaluating`); // Initialize the primary index, which won't show up in the changefeed indexes.push(index.primary_index_name); @@ -59,7 +59,7 @@ class Table { indexes.forEach((name) => { try { const old_index = this.indexes.get(name); - const new_index = new index.Index(this.logger, name, this.table, conn); + const new_index = new index.Index(name, this.table, conn); if (old_index) { // Steal any waiters from the old index new_index._waiters = old_index._waiters; @@ -67,13 +67,13 @@ class Table { } new_index_map.set(name, new_index); } catch (err) { - this.logger.warn(`${err}`); + logger.warn(`${err}`); } }); this.indexes.forEach((i) => i.close()); this.indexes = new_index_map; - this.logger.debug(`${this.table} indexes updated`); + logger.debug(`${this.table} indexes updated`); } // TODO: support geo and multi indexes @@ -85,7 +85,7 @@ class Table { const success = () => { // Create the Index object now so we don't try to create it again before the // feed notifies us of the index creation - const new_index = new index.Index(this.logger, index_name, this.table, conn); + const new_index = new index.Index(index_name, this.table, conn); // TODO: shouldn't this be done before we go async? this.indexes.set(index_name, new_index); return new_index.on_ready(done); @@ -96,7 +96,7 @@ class Table { .run(conn) .then(success) .catch((err) => { - if (err instanceof this.r.Error.ReqlError && + if (err instanceof r.Error.ReqlError && err.msg.indexOf('already exists') !== -1) { success(); } else { diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index f315a160a..04e911acc 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -24,6 +24,10 @@ "dependencies": { "@horizon/client": "2.0.0-beta-5" }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index ce8aee847..94a4fbde4 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -4,6 +4,8 @@ const Rule = require('./rule'); const assert = require('assert'); +const {r, logger, ReliableChangefeed} = require('@horizon/server'); + // We can be desynced from the database for up to 5 seconds before we // start rejecting queries. const staleMs = 5000; @@ -121,9 +123,6 @@ class RuleMap { class UserCache { constructor(config, ctx) { - const r = ctx.r; - - this.ctx = ctx; this.timeout = config.cacheTimeout; this.ruleMap = new RuleMap(); @@ -131,7 +130,7 @@ class UserCache { this.userCfeeds = new Map(); this.newUserCfeed = (userId) => { let oldGroups = new Set(); - const cfeed = new ctx.ReliableChangefeed( + const cfeed = new ReliableChangefeed( r.table(config.usersTable).get(userId).changes({includeInitial: true}), ctx.rdb_connection(), { @@ -164,7 +163,7 @@ class UserCache { this.queuedGroups = new Map(); this.groupsUnreadyAt = new Date(0); // epoch - this.groupCfeed = new ctx.ReliableChangefeed( + this.groupCfeed = new ReliableChangefeed( r.table(config.groupsTable).changes({includeInitial: true}), ctx.rdb_connection(), { @@ -280,7 +279,7 @@ class UserCache { } catch (err) { // We don't want to pass the error message on to the user because // it might leak information about the data. - this.ctx.logger.error(`Exception in validator function: ${err.stack}`); + logger.error(`Exception in validator function: ${err.stack}`); } }; }; @@ -316,7 +315,7 @@ module.exports = (raw_config) => { name, activate(ctx) { - ctx.logger.info('Activating permissions plugin.'); + logger.info('Activating permissions plugin.'); ctx[userCache] = new UserCache(config, ctx); authCb = (clientCtx) => { diff --git a/plugins/permit_all/package.json b/plugins/permit_all/package.json index db6cfb16d..9b5471d54 100644 --- a/plugins/permit_all/package.json +++ b/plugins/permit_all/package.json @@ -23,6 +23,9 @@ }, "dependencies": { }, + "peerDependencies": { + "@horizon/plugin-router": "1.x" + }, "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", diff --git a/plugins/reads/package.json b/plugins/reads/package.json index dd96c1e13..e86ce0896 100644 --- a/plugins/reads/package.json +++ b/plugins/reads/package.json @@ -23,6 +23,10 @@ }, "dependencies": { }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", diff --git a/plugins/reads/src/common.js b/plugins/reads/src/common.js index 09cffd447..9312fc990 100644 --- a/plugins/reads/src/common.js +++ b/plugins/reads/src/common.js @@ -1,5 +1,7 @@ 'use strict'; +const {r} = require('@horizon/server'); + const reql_options = { timeFormat: 'raw', binaryFormat: 'raw', @@ -21,7 +23,7 @@ function object_to_fields(obj) { } // This is exposed to be reused by 'subscribe' -const make_reql = (r, req) => Promise.resolve().then(() => { +const make_reql = (req) => Promise.resolve().then(() => { const find = req.getParameter('find'); const limit = req.getParameter('limit'); const order = req.getParameter('order'); diff --git a/plugins/reads/src/fetch.js b/plugins/reads/src/fetch.js index 8b73ab930..8651115d5 100644 --- a/plugins/reads/src/fetch.js +++ b/plugins/reads/src/fetch.js @@ -12,7 +12,7 @@ module.exports = (server) => (req, res, next) => { } else if (!permissions) { next(new Error('"fetch" requires permissions to run')); } else { - common.make_reql(server.r, req).then((reql) => + common.make_reql(req).then((reql) => reql.run(conn, common.reql_options) ).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { diff --git a/plugins/reads/src/watch.js b/plugins/reads/src/watch.js index 0efc067e8..8c28a198d 100644 --- a/plugins/reads/src/watch.js +++ b/plugins/reads/src/watch.js @@ -12,7 +12,7 @@ module.exports = (server) => (req, res, next) => { } else if (!permissions) { next(new Error('"watch" requires permissions to run')); } else { - common.make_reql(server.r, req).then((reql) => + common.make_reql(req).then((reql) => reql.changes({ include_initial: true, include_states: true, diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json index e908d1330..6bb66158d 100644 --- a/plugins/timeout/package.json +++ b/plugins/timeout/package.json @@ -23,6 +23,9 @@ }, "dependencies": { }, + "peerDependencies": { + "@horizon/plugin-router": "1.x" + }, "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", diff --git a/plugins/writes/package.json b/plugins/writes/package.json index 4f2c3d062..61678755f 100644 --- a/plugins/writes/package.json +++ b/plugins/writes/package.json @@ -23,6 +23,10 @@ }, "dependencies": { }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", diff --git a/plugins/writes/src/common.js b/plugins/writes/src/common.js index d1505280c..266170412 100644 --- a/plugins/writes/src/common.js +++ b/plugins/writes/src/common.js @@ -2,6 +2,8 @@ const assert = require('assert'); +const {r} = require('@horizon/server'); + // Common functionality used by write requests const reql_options = { @@ -15,7 +17,7 @@ const timeout_msg = 'Operation timed out.'; const unauthorized_msg = 'Operation not permitted.'; const hz_v = '$hz_v$'; -function apply_version(r, row, new_version) { +function apply_version(row, new_version) { row.merge(r.object(hz_v, new_version)); } diff --git a/plugins/writes/src/insert.js b/plugins/writes/src/insert.js index 09003c609..949c097fa 100644 --- a/plugins/writes/src/insert.js +++ b/plugins/writes/src/insert.js @@ -2,8 +2,9 @@ const common = require('./common'); +const {r} = require('@horizon/server'); + module.exports = (server) => (request, response, next) => { - const r = server.r; const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); @@ -25,7 +26,7 @@ module.exports = (server) => (request, response, next) => { }, (rows) => // write to database, all valid rows collection.table - .insert(rows.map((row) => common.apply_version(r, r.expr(row), 0)), + .insert(rows.map((row) => common.apply_version(r.expr(row), 0)), {returnChanges: 'always'}) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/writes/src/remove.js b/plugins/writes/src/remove.js index bad3db16b..f66472476 100644 --- a/plugins/writes/src/remove.js +++ b/plugins/writes/src/remove.js @@ -3,8 +3,9 @@ const common = require('./common'); const hz_v = common.version_field; +const {r} = require('@horizon/server'); + module.exports = (server) => (request, response, next) => { - const r = server.r; const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); diff --git a/plugins/writes/src/replace.js b/plugins/writes/src/replace.js index 11019218e..3052e06a7 100644 --- a/plugins/writes/src/replace.js +++ b/plugins/writes/src/replace.js @@ -3,8 +3,9 @@ const common = require('./common'); const hz_v = common.version_field; +const {r} = require('@horizon/server'); + module.exports = (server) => (request, response, next) => { - const r = server.r; const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); @@ -38,7 +39,7 @@ module.exports = (server) => (request, response, next) => { // Otherwise, we can safely replace the row common.apply_version( - r, new_row, old_row(hz_v).default(-1).add(1))), + new_row, old_row(hz_v).default(-1).add(1))), {returnChanges: 'always'})) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/writes/src/store.js b/plugins/writes/src/store.js index 90f93569a..2edc59a60 100644 --- a/plugins/writes/src/store.js +++ b/plugins/writes/src/store.js @@ -3,8 +3,9 @@ const common = require('./common'); const hz_v = common.version_field; +const {r} = require('@horizon/server'); + module.exports = (server) => (request, response, next) => { - const r = server.r; const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); @@ -36,7 +37,7 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, insert the row - common.apply_version(r, new_row, 0) + common.apply_version(new_row, 0) ), r.branch( // The row may have changed from the expected version @@ -45,14 +46,13 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, we can overwrite the row - common.apply_version(r, - new_row, + common.apply_version(new_row, old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row does not have an id, so we insert it with an autogen id - collection.table.insert(common.apply_version(r, new_row, 0), + collection.table.insert(common.apply_version(new_row, 0), {returnChanges: 'always'}))) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/writes/src/update.js b/plugins/writes/src/update.js index 842e882e6..71c4c6a62 100644 --- a/plugins/writes/src/update.js +++ b/plugins/writes/src/update.js @@ -3,8 +3,9 @@ const common = require('./common'); const hz_v = common.version_field; +const {r} = require('@horizon/server'); + module.exports = (server) => (request, response, next) => { - const r = server.r; const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); @@ -42,8 +43,7 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise we can update the row and increment the version - common.apply_version(r, - old_row.merge(new_row), + common.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1))), {returnChanges: 'always'})) .run(conn, common.reql_options) diff --git a/plugins/writes/src/upsert.js b/plugins/writes/src/upsert.js index f0ce9e4b1..8bf45e2f8 100644 --- a/plugins/writes/src/upsert.js +++ b/plugins/writes/src/upsert.js @@ -3,8 +3,9 @@ const common = require('./common'); const hz_v = common.version_field; +const {r} = require('@horizon/server'); + module.exports = (server) => (request, response, next) => { - const r = server.r; const conn = server.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); @@ -43,7 +44,7 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, insert the row - common.apply_version(r, new_row, 0) + common.apply_version(new_row, 0) ), r.branch( // The row may have changed from the expected version @@ -52,14 +53,13 @@ module.exports = (server) => (request, response, next) => { r.error(common.invalidated_msg), // Otherwise, we can update the row and increment the version - common.apply_version(r, - old_row.merge(new_row), + common.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row did not have an id, so we insert it with an autogen id - collection.table.insert(common.apply_version(r, new_row, 0), + collection.table.insert(common.apply_version(new_row, 0), {returnChanges: 'always'}))) .run(conn, common.reql_options) ).then((msg) => response.end(msg)).catch(next); diff --git a/server-utils/.babelrc b/server-utils/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/server-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/server-utils/.eslintrc.js b/server-utils/.eslintrc.js deleted file mode 100644 index 98faeb27b..000000000 --- a/server-utils/.eslintrc.js +++ /dev/null @@ -1,17 +0,0 @@ -const OFF = 0; -const WARN = 1; -const ERROR = 2; - -module.exports = { - extends: "../.eslintrc.js", - rules: { - "camelcase": [ ERROR ], - "max-len": [ ERROR, 130 ], - "prefer-template": [ OFF ], - }, - env: { - "es6": true, - "node": true, - "mocha": true, - }, -}; diff --git a/server-utils/package.json b/server-utils/package.json deleted file mode 100644 index aa8a75a50..000000000 --- a/server-utils/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@horizon/server-utils", - "version": "1.0.0", - "description": "Utilities for the Horizon server and its plugins.", - "main": "dist/utils.js", - "scripts": { - "lint": "eslint src test", - "test": "mocha dist/test --timeout 10000", - "coverage": "istanbul cover _mocha test/test.js", - "build": "babel src -d dist -s true" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rethinkdb/horizon.git" - }, - "author": "RethinkDB", - "license": "MIT", - "bugs": { - "url": "https://github.com/rethinkdb/horizon/issues" - }, - "homepage": "https://github.com/rethinkdb/horizon#readme", - "engines": { - "node": ">=4.0.0" - }, - "dependencies": { - "rethinkdb": "^2.1.1" - }, - "devDependencies": { - "eslint": "^2.3.0", - "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.9.0" - } -} diff --git a/server-utils/src/reliable.js b/server-utils/src/reliable.js deleted file mode 100644 index f7661de41..000000000 --- a/server-utils/src/reliable.js +++ /dev/null @@ -1,119 +0,0 @@ -'use strict'; - -import * as r from 'rethinkdb'; - -const subs = Symbol('subs'); - -export class Reliable { - constructor(initialCbs) { - this[subs] = new Map(); - this.ready = false; - this.closed = false; - if (initialCbs) { - this.subscribe(initialCbs); - } - } - - numSubs() { - return this[subs].size; - } - - subscribe(cbs) { - if (this.closed) { - throw new Error('Cannot subscribe to a closed ReliableConn.'); - } - const subId = Symbol(); - this[subs].set(subId, { - cbs: cbs, - close: () => this[subs].delete(subId), - }); - if (this.ready && cbs.onReady) { - try { - cbs.onReady.apply(cbs, this.ready); - } catch (e) { - // RSI: use logging facilities - console.error('Unexpected error in reliable callback, ' + - `event: subscribe onReady, error: ${e.stack}`); - } - } - return this[subs].get(subId); - } - - emit() { - if (!this.closed) { - this.emitInternal.apply(this, arguments); - } - } - - emitInternal(eventType, ...args) { - // TODO: consider checking to make sure we don't send two - // `onReady` or `onUnready`s in a row (or even just returning - // early if we would). - if (eventType === 'onReady') { - this.ready = args; - } else if (eventType === 'onUnready') { - this.ready = false; - } - this[subs].forEach((sub) => { - try { - const event = sub.cbs[eventType]; - if (event) { - event.apply(sub.cbs, args); - } - } catch (e) { - // RSI: use logging facilities - console.error('Unexpected error in reliable callback, ' + - `event: ${eventType}, error: ${e.stack}`); - } - }); - } - - close(reason) { - this.closed = true; - if (this.ready) { - this.emitInternal('onUnready', new Error(`closed: ${reason}`)); - } - this[subs].clear(); // Just to make sure no subclasses do anything clever. - return Promise.resolve(); - } -} - -export class ReliableUnion extends Reliable { - constructor(reqs, cbs) { - super(cbs); - this.reqs = reqs; - this.subs = {}; - this.emitArg = {}; - this.readyNeeded = 0; - for (const k in reqs) { - this.subs[k] = reqs[k].subscribe({ - onReady: (...rest) => { - this.readyNeeded -= 1; - this.emitArg[k] = rest; - this.maybeEmit(); - }, - onUnready: (...rest) => { - this.readyNeeded += 1; - this.emitArg[k] = rest; - this.maybeEmit(); - }, - }); - this.readyNeeded += 1; - } - } - - maybeEmit() { - if (this.readyNeeded === 0 && !this.ready) { - this.emit('onReady', this.emitArg); - } else if (this.readyNeeded !== 0 && this.ready) { - this.emit('onUnready', this.emitArg); - } - } - - close(reason) { - for (const k in this.subs) { - this.subs[k].close(); - } - return super.close(reason); - } -} diff --git a/server-utils/src/utils.js b/server-utils/src/utils.js deleted file mode 100644 index c5c79e07e..000000000 --- a/server-utils/src/utils.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -export {Reliable, ReliableUnion} from './reliable'; diff --git a/server/src/index.js b/server/src/index.js index 90863e9e2..896b76a6c 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -18,14 +18,9 @@ module.exports.Server = server.Server; module.exports.r = require('rethinkdb'); module.exports.logger = require('./logger'); -module.exports.utils = require('./utils'); -module.exports.auth = { - auth0: require('./auth/auth0'), - facebook: require('./auth/facebook'), - github: require('./auth/github'), - google: require('./auth/google'), - slack: require('./auth/slack'), - twitch: require('./auth/twitch'), - twitter: require('./auth/twitter'), -}; +const reliable = require('./reliable'); +module.exports.Reliable = reliable.Reliable; +module.exports.ReliableConn = reliable.ReliableConn; +module.exports.ReliableUnion = reliable.ReliableUnion; +module.exports.ReliableChangefeed = reliable.ReliableChangefeed; diff --git a/server/src/reliable.js b/server/src/reliable.js index 22ba7e6ad..66196e09e 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -1,9 +1,124 @@ 'use strict'; import * as r from 'rethinkdb'; -import {Reliable} from '@horizon/server-utils'; const logger = require('./logger'); +const subs = Symbol('subs'); + +export class Reliable { + constructor(initialCbs) { + this[subs] = new Map(); + this.ready = false; + this.closed = false; + if (initialCbs) { + this.subscribe(initialCbs); + } + } + + numSubs() { + return this[subs].size; + } + + subscribe(cbs) { + if (this.closed) { + throw new Error('Cannot subscribe to a closed ReliableConn.'); + } + const subId = Symbol(); + this[subs].set(subId, { + cbs: cbs, + close: () => this[subs].delete(subId), + }); + if (this.ready && cbs.onReady) { + try { + cbs.onReady.apply(cbs, this.ready); + } catch (e) { + // RSI: use logging facilities + console.error('Unexpected error in reliable callback, ' + + `event: subscribe onReady, error: ${e.stack}`); + } + } + return this[subs].get(subId); + } + + emit() { + if (!this.closed) { + this.emitInternal.apply(this, arguments); + } + } + + emitInternal(eventType, ...args) { + // TODO: consider checking to make sure we don't send two + // `onReady` or `onUnready`s in a row (or even just returning + // early if we would). + if (eventType === 'onReady') { + this.ready = args; + } else if (eventType === 'onUnready') { + this.ready = false; + } + this[subs].forEach((sub) => { + try { + const event = sub.cbs[eventType]; + if (event) { + event.apply(sub.cbs, args); + } + } catch (e) { + // RSI: use logging facilities + console.error('Unexpected error in reliable callback, ' + + `event: ${eventType}, error: ${e.stack}`); + } + }); + } + + close(reason) { + this.closed = true; + if (this.ready) { + this.emitInternal('onUnready', new Error(`closed: ${reason}`)); + } + this[subs].clear(); // Just to make sure no subclasses do anything clever. + return Promise.resolve(); + } +} + +export class ReliableUnion extends Reliable { + constructor(reqs, cbs) { + super(cbs); + this.reqs = reqs; + this.subs = {}; + this.emitArg = {}; + this.readyNeeded = 0; + for (const k in reqs) { + this.subs[k] = reqs[k].subscribe({ + onReady: (...rest) => { + this.readyNeeded -= 1; + this.emitArg[k] = rest; + this.maybeEmit(); + }, + onUnready: (...rest) => { + this.readyNeeded += 1; + this.emitArg[k] = rest; + this.maybeEmit(); + }, + }); + this.readyNeeded += 1; + } + } + + maybeEmit() { + if (this.readyNeeded === 0 && !this.ready) { + this.emit('onReady', this.emitArg); + } else if (this.readyNeeded !== 0 && this.ready) { + this.emit('onUnready', this.emitArg); + } + } + + close(reason) { + for (const k in this.subs) { + this.subs[k].close(); + } + return super.close(reason); + } +} + export class ReliableConn extends Reliable { constructor(connOpts) { super(); From 06c79d02dcd75aacd34f5d9e7a86e1f0d8e5c3fe Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 1 Sep 2016 23:36:26 -0700 Subject: [PATCH 47/86] restructuring plugins module --- plugins/{collection => }/.babelrc | 0 plugins/collection/package.json | 37 ------------- plugins/{reads => }/package.json | 8 +-- plugins/permissions/.babelrc | 3 -- plugins/permissions/package.json | 38 ------------- plugins/permit_all/.babelrc | 3 -- plugins/permit_all/package.json | 36 ------------- plugins/reads/.babelrc | 3 -- .../src => src/collection}/collection.js | 0 .../src => src/collection}/index.js | 0 .../src => src/collection}/main.js | 0 .../src => src/collection}/metadata.js | 0 .../src => src/collection}/table.js | 0 .../src => src/collection}/utils.js | 0 plugins/{writes => }/src/common.js | 0 plugins/src/index.js | 53 +++++++++++++++++++ plugins/{writes => }/src/insert.js | 0 .../src => src/permissions}/group.js | 0 .../permissions/main.js} | 0 .../src => src/permissions}/remake_error.js | 0 .../src => src/permissions}/rule.js | 0 .../src => src/permissions}/template.js | 0 .../src => src/permissions}/validator.js | 0 .../src => src/permit_all}/index.js | 0 plugins/{reads/src => src/reads}/above.js | 0 plugins/{reads/src => src/reads}/below.js | 0 plugins/{reads/src => src/reads}/common.js | 0 plugins/{reads/src => src/reads}/fetch.js | 0 plugins/{reads/src => src/reads}/find.js | 0 plugins/{reads/src => src/reads}/findAll.js | 0 plugins/{reads/src => src/reads}/index.js | 0 plugins/{reads/src => src/reads}/limit.js | 0 plugins/{reads/src => src/reads}/order.js | 0 plugins/{reads/src => src/reads}/watch.js | 0 plugins/{writes => }/src/remove.js | 0 plugins/{writes => }/src/replace.js | 0 plugins/{writes => }/src/store.js | 0 plugins/{timeout/src => src/timeout}/index.js | 0 plugins/{writes => }/src/update.js | 0 plugins/{writes => }/src/upsert.js | 0 plugins/{writes/src => src/writes}/index.js | 0 plugins/timeout/.babelrc | 3 -- plugins/timeout/package.json | 36 ------------- plugins/writes/.babelrc | 3 -- plugins/writes/package.json | 37 ------------- 45 files changed, 57 insertions(+), 203 deletions(-) rename plugins/{collection => }/.babelrc (100%) delete mode 100644 plugins/collection/package.json rename plugins/{reads => }/package.json (81%) delete mode 100644 plugins/permissions/.babelrc delete mode 100644 plugins/permissions/package.json delete mode 100644 plugins/permit_all/.babelrc delete mode 100644 plugins/permit_all/package.json delete mode 100644 plugins/reads/.babelrc rename plugins/{collection/src => src/collection}/collection.js (100%) rename plugins/{collection/src => src/collection}/index.js (100%) rename plugins/{collection/src => src/collection}/main.js (100%) rename plugins/{collection/src => src/collection}/metadata.js (100%) rename plugins/{collection/src => src/collection}/table.js (100%) rename plugins/{collection/src => src/collection}/utils.js (100%) rename plugins/{writes => }/src/common.js (100%) create mode 100644 plugins/src/index.js rename plugins/{writes => }/src/insert.js (100%) rename plugins/{permissions/src => src/permissions}/group.js (100%) rename plugins/{permissions/src/permissions.js => src/permissions/main.js} (100%) rename plugins/{permissions/src => src/permissions}/remake_error.js (100%) rename plugins/{permissions/src => src/permissions}/rule.js (100%) rename plugins/{permissions/src => src/permissions}/template.js (100%) rename plugins/{permissions/src => src/permissions}/validator.js (100%) rename plugins/{permit_all/src => src/permit_all}/index.js (100%) rename plugins/{reads/src => src/reads}/above.js (100%) rename plugins/{reads/src => src/reads}/below.js (100%) rename plugins/{reads/src => src/reads}/common.js (100%) rename plugins/{reads/src => src/reads}/fetch.js (100%) rename plugins/{reads/src => src/reads}/find.js (100%) rename plugins/{reads/src => src/reads}/findAll.js (100%) rename plugins/{reads/src => src/reads}/index.js (100%) rename plugins/{reads/src => src/reads}/limit.js (100%) rename plugins/{reads/src => src/reads}/order.js (100%) rename plugins/{reads/src => src/reads}/watch.js (100%) rename plugins/{writes => }/src/remove.js (100%) rename plugins/{writes => }/src/replace.js (100%) rename plugins/{writes => }/src/store.js (100%) rename plugins/{timeout/src => src/timeout}/index.js (100%) rename plugins/{writes => }/src/update.js (100%) rename plugins/{writes => }/src/upsert.js (100%) rename plugins/{writes/src => src/writes}/index.js (100%) delete mode 100644 plugins/timeout/.babelrc delete mode 100644 plugins/timeout/package.json delete mode 100644 plugins/writes/.babelrc delete mode 100644 plugins/writes/package.json diff --git a/plugins/collection/.babelrc b/plugins/.babelrc similarity index 100% rename from plugins/collection/.babelrc rename to plugins/.babelrc diff --git a/plugins/collection/package.json b/plugins/collection/package.json deleted file mode 100644 index 21466cbba..000000000 --- a/plugins/collection/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@horizon/plugin-collection", - "version": "1.0.0", - "description": "Plugin that gets the collection for Horizon operations.", - "main": "dist/main.js", - "scripts": { - "lint": "eslint src test", - "test": "mocha test", - "build": "babel src -d dist -s true" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rethinkdb/horizon.git" - }, - "author": "RethinkDB", - "license": "MIT", - "bugs": { - "url": "https://github.com/rethinkdb/horizon/issues" - }, - "homepage": "https://github.com/rethinkdb/horizon#readme", - "engines": { - "node": ">=4.0.0" - }, - "dependencies": { - }, - "peerDependencies": { - "@horizon/plugin-router": "1.x", - "@horizon/server": "3.x" - }, - "devDependencies": { - "eslint": "^2.3.0", - "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.9.0" - } -} diff --git a/plugins/reads/package.json b/plugins/package.json similarity index 81% rename from plugins/reads/package.json rename to plugins/package.json index e86ce0896..f783e5250 100644 --- a/plugins/reads/package.json +++ b/plugins/package.json @@ -1,11 +1,11 @@ { - "name": "@horizon/plugin-reads", + "name": "@horizon/default-plugins", "version": "1.0.0", - "description": "Plugin that provides default Horizon read operations.", + "description": "Plugins for the default Horizon Collections API.", "main": "dist/index.js", "scripts": { - "lint": "eslint src test", - "test": "mocha test", + "lint": "eslint src", + "test": "mocha dist/test", "build": "babel src -d dist -s true" }, "repository": { diff --git a/plugins/permissions/.babelrc b/plugins/permissions/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/permissions/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json deleted file mode 100644 index 04e911acc..000000000 --- a/plugins/permissions/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@horizon/plugin-permissions", - "version": "1.0.0", - "description": "Default permissions plugin for Horizon.", - "main": "dist/permissions.js", - "scripts": { - "lint": "eslint src test", - "test": "mocha test", - "build": "babel src -d dist -s true" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rethinkdb/horizon.git" - }, - "author": "RethinkDB", - "license": "MIT", - "bugs": { - "url": "https://github.com/rethinkdb/horizon/issues" - }, - "homepage": "https://github.com/rethinkdb/horizon#readme", - "engines": { - "node": ">=4.0.0" - }, - "dependencies": { - "@horizon/client": "2.0.0-beta-5" - }, - "peerDependencies": { - "@horizon/plugin-router": "1.x", - "@horizon/server": "3.x" - }, - "devDependencies": { - "eslint": "^2.3.0", - "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.9.0" - } -} diff --git a/plugins/permit_all/.babelrc b/plugins/permit_all/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/permit_all/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/permit_all/package.json b/plugins/permit_all/package.json deleted file mode 100644 index 9b5471d54..000000000 --- a/plugins/permit_all/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@horizon/plugin-permit-all", - "version": "1.0.0", - "description": "Plugin that permits all Horizon operations.", - "main": "dist/index.js", - "scripts": { - "lint": "eslint src test", - "test": "mocha test", - "build": "babel src -d dist -s true" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rethinkdb/horizon.git" - }, - "author": "RethinkDB", - "license": "MIT", - "bugs": { - "url": "https://github.com/rethinkdb/horizon/issues" - }, - "homepage": "https://github.com/rethinkdb/horizon#readme", - "engines": { - "node": ">=4.0.0" - }, - "dependencies": { - }, - "peerDependencies": { - "@horizon/plugin-router": "1.x" - }, - "devDependencies": { - "eslint": "^2.3.0", - "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.9.0" - } -} diff --git a/plugins/reads/.babelrc b/plugins/reads/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/reads/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/collection/src/collection.js b/plugins/src/collection/collection.js similarity index 100% rename from plugins/collection/src/collection.js rename to plugins/src/collection/collection.js diff --git a/plugins/collection/src/index.js b/plugins/src/collection/index.js similarity index 100% rename from plugins/collection/src/index.js rename to plugins/src/collection/index.js diff --git a/plugins/collection/src/main.js b/plugins/src/collection/main.js similarity index 100% rename from plugins/collection/src/main.js rename to plugins/src/collection/main.js diff --git a/plugins/collection/src/metadata.js b/plugins/src/collection/metadata.js similarity index 100% rename from plugins/collection/src/metadata.js rename to plugins/src/collection/metadata.js diff --git a/plugins/collection/src/table.js b/plugins/src/collection/table.js similarity index 100% rename from plugins/collection/src/table.js rename to plugins/src/collection/table.js diff --git a/plugins/collection/src/utils.js b/plugins/src/collection/utils.js similarity index 100% rename from plugins/collection/src/utils.js rename to plugins/src/collection/utils.js diff --git a/plugins/writes/src/common.js b/plugins/src/common.js similarity index 100% rename from plugins/writes/src/common.js rename to plugins/src/common.js diff --git a/plugins/src/index.js b/plugins/src/index.js new file mode 100644 index 000000000..c8c50b3b6 --- /dev/null +++ b/plugins/src/index.js @@ -0,0 +1,53 @@ +'use strict'; + +const allPlugins = { + // Collections API + above: require('./above'), + below: require('./below'), + collection: require('./collection'), + insert: require('./insert'), + fetch: require('./fetch'), + find: require('./find'), + findAll: require('./findAll'), + limit: require('./limit'), + order: require('./order'), + remove: require('./remove'), + replace: require('./replace'), + store: require('./store'), + timeout: require('./timeout'), + update: require('./update'), + upsert: require('./upsert'), + watch: require('./watch'), + + // Permissions API + permissions: require('./permissions'), + permit_all: require('./permit_all'), +}; + + +module.exports = function(options) { + options.methods = options.methods || Object.keys(allPlugins); + const subplugins = options.methods.map((name) => { + if (!allPlugins[name]) { + throw new Error(`"${name}" is not a default Horizon method.`); + } + return allPlugins[name]; + }); + + return { + name: 'hz_default_plugins', + activate: (ctx) => + Promise.all(subplugins.map((p) => + Promise.resolve().then(() => p.activate(ctx)))).then((results) => ({ + methods: Object.assign({}, results.methods), + })), + + deactivate: (ctx) => + Promise.all(subplugins.map((p) => + Promise.resolve().then(() => p.deactivate(ctx)))), + }; +}; + +for (const name of allPlugins) { + module.exports[name] = allPlugins[name]; +} diff --git a/plugins/writes/src/insert.js b/plugins/src/insert.js similarity index 100% rename from plugins/writes/src/insert.js rename to plugins/src/insert.js diff --git a/plugins/permissions/src/group.js b/plugins/src/permissions/group.js similarity index 100% rename from plugins/permissions/src/group.js rename to plugins/src/permissions/group.js diff --git a/plugins/permissions/src/permissions.js b/plugins/src/permissions/main.js similarity index 100% rename from plugins/permissions/src/permissions.js rename to plugins/src/permissions/main.js diff --git a/plugins/permissions/src/remake_error.js b/plugins/src/permissions/remake_error.js similarity index 100% rename from plugins/permissions/src/remake_error.js rename to plugins/src/permissions/remake_error.js diff --git a/plugins/permissions/src/rule.js b/plugins/src/permissions/rule.js similarity index 100% rename from plugins/permissions/src/rule.js rename to plugins/src/permissions/rule.js diff --git a/plugins/permissions/src/template.js b/plugins/src/permissions/template.js similarity index 100% rename from plugins/permissions/src/template.js rename to plugins/src/permissions/template.js diff --git a/plugins/permissions/src/validator.js b/plugins/src/permissions/validator.js similarity index 100% rename from plugins/permissions/src/validator.js rename to plugins/src/permissions/validator.js diff --git a/plugins/permit_all/src/index.js b/plugins/src/permit_all/index.js similarity index 100% rename from plugins/permit_all/src/index.js rename to plugins/src/permit_all/index.js diff --git a/plugins/reads/src/above.js b/plugins/src/reads/above.js similarity index 100% rename from plugins/reads/src/above.js rename to plugins/src/reads/above.js diff --git a/plugins/reads/src/below.js b/plugins/src/reads/below.js similarity index 100% rename from plugins/reads/src/below.js rename to plugins/src/reads/below.js diff --git a/plugins/reads/src/common.js b/plugins/src/reads/common.js similarity index 100% rename from plugins/reads/src/common.js rename to plugins/src/reads/common.js diff --git a/plugins/reads/src/fetch.js b/plugins/src/reads/fetch.js similarity index 100% rename from plugins/reads/src/fetch.js rename to plugins/src/reads/fetch.js diff --git a/plugins/reads/src/find.js b/plugins/src/reads/find.js similarity index 100% rename from plugins/reads/src/find.js rename to plugins/src/reads/find.js diff --git a/plugins/reads/src/findAll.js b/plugins/src/reads/findAll.js similarity index 100% rename from plugins/reads/src/findAll.js rename to plugins/src/reads/findAll.js diff --git a/plugins/reads/src/index.js b/plugins/src/reads/index.js similarity index 100% rename from plugins/reads/src/index.js rename to plugins/src/reads/index.js diff --git a/plugins/reads/src/limit.js b/plugins/src/reads/limit.js similarity index 100% rename from plugins/reads/src/limit.js rename to plugins/src/reads/limit.js diff --git a/plugins/reads/src/order.js b/plugins/src/reads/order.js similarity index 100% rename from plugins/reads/src/order.js rename to plugins/src/reads/order.js diff --git a/plugins/reads/src/watch.js b/plugins/src/reads/watch.js similarity index 100% rename from plugins/reads/src/watch.js rename to plugins/src/reads/watch.js diff --git a/plugins/writes/src/remove.js b/plugins/src/remove.js similarity index 100% rename from plugins/writes/src/remove.js rename to plugins/src/remove.js diff --git a/plugins/writes/src/replace.js b/plugins/src/replace.js similarity index 100% rename from plugins/writes/src/replace.js rename to plugins/src/replace.js diff --git a/plugins/writes/src/store.js b/plugins/src/store.js similarity index 100% rename from plugins/writes/src/store.js rename to plugins/src/store.js diff --git a/plugins/timeout/src/index.js b/plugins/src/timeout/index.js similarity index 100% rename from plugins/timeout/src/index.js rename to plugins/src/timeout/index.js diff --git a/plugins/writes/src/update.js b/plugins/src/update.js similarity index 100% rename from plugins/writes/src/update.js rename to plugins/src/update.js diff --git a/plugins/writes/src/upsert.js b/plugins/src/upsert.js similarity index 100% rename from plugins/writes/src/upsert.js rename to plugins/src/upsert.js diff --git a/plugins/writes/src/index.js b/plugins/src/writes/index.js similarity index 100% rename from plugins/writes/src/index.js rename to plugins/src/writes/index.js diff --git a/plugins/timeout/.babelrc b/plugins/timeout/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/timeout/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json deleted file mode 100644 index 6bb66158d..000000000 --- a/plugins/timeout/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@horizon/plugin-timeout", - "version": "1.0.0", - "description": "Converts timeouts to deadlines for Horizon write operations.", - "main": "dist/index.js", - "scripts": { - "lint": "eslint src test", - "test": "mocha test", - "build": "babel src -d dist -s true" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rethinkdb/horizon.git" - }, - "author": "RethinkDB", - "license": "MIT", - "bugs": { - "url": "https://github.com/rethinkdb/horizon/issues" - }, - "homepage": "https://github.com/rethinkdb/horizon#readme", - "engines": { - "node": ">=4.0.0" - }, - "dependencies": { - }, - "peerDependencies": { - "@horizon/plugin-router": "1.x" - }, - "devDependencies": { - "eslint": "^2.3.0", - "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.9.0" - } -} diff --git a/plugins/writes/.babelrc b/plugins/writes/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/writes/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/writes/package.json b/plugins/writes/package.json deleted file mode 100644 index 61678755f..000000000 --- a/plugins/writes/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@horizon/plugin-writes", - "version": "1.0.0", - "description": "Plugin that provides default Horizon write operations.", - "main": "dist/index.js", - "scripts": { - "lint": "eslint src test", - "test": "mocha test", - "build": "babel src -d dist -s true" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/rethinkdb/horizon.git" - }, - "author": "RethinkDB", - "license": "MIT", - "bugs": { - "url": "https://github.com/rethinkdb/horizon/issues" - }, - "homepage": "https://github.com/rethinkdb/horizon#readme", - "engines": { - "node": ">=4.0.0" - }, - "dependencies": { - }, - "peerDependencies": { - "@horizon/plugin-router": "1.x", - "@horizon/server": "3.x" - }, - "devDependencies": { - "eslint": "^2.3.0", - "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", - "babel-preset-es2015": "^6.9.0" - } -} From 7ce2ceacaa83d143f3550aed899972d0e37cf0a6 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 1 Sep 2016 23:58:15 -0700 Subject: [PATCH 48/86] more plugins module resturcturing --- plugins/src/{reads => }/above.js | 16 ++++- plugins/src/{reads => }/below.js | 16 ++++- .../src/{collection/main.js => collection.js} | 0 .../src/{collection => common}/collection.js | 0 plugins/src/{permissions => common}/group.js | 0 plugins/src/{collection => common}/index.js | 0 .../src/{collection => common}/metadata.js | 0 .../src/{reads/common.js => common/reads.js} | 0 .../{permissions => common}/remake_error.js | 0 plugins/src/{permissions => common}/rule.js | 0 plugins/src/{collection => common}/table.js | 0 .../src/{permissions => common}/template.js | 0 plugins/src/{collection => common}/utils.js | 0 .../src/{permissions => common}/validator.js | 0 plugins/src/{common.js => common/writes.js} | 0 plugins/src/fetch.js | 67 +++++++++++++++++++ plugins/src/{reads => }/find.js | 16 ++++- plugins/src/{reads => }/findAll.js | 16 ++++- plugins/src/index.js | 6 +- plugins/src/{reads => }/limit.js | 16 ++++- plugins/src/{reads => }/order.js | 16 ++++- .../{permissions/main.js => permissions.js} | 0 .../{permit_all/index.js => permit_all.js} | 0 plugins/src/reads/fetch.js | 52 -------------- plugins/src/reads/index.js | 52 -------------- plugins/src/reads/watch.js | 48 ------------- plugins/src/{timeout/index.js => timeout.js} | 0 plugins/src/watch.js | 63 +++++++++++++++++ 28 files changed, 217 insertions(+), 167 deletions(-) rename plugins/src/{reads => }/above.js (74%) rename plugins/src/{reads => }/below.js (74%) rename plugins/src/{collection/main.js => collection.js} (100%) rename plugins/src/{collection => common}/collection.js (100%) rename plugins/src/{permissions => common}/group.js (100%) rename plugins/src/{collection => common}/index.js (100%) rename plugins/src/{collection => common}/metadata.js (100%) rename plugins/src/{reads/common.js => common/reads.js} (100%) rename plugins/src/{permissions => common}/remake_error.js (100%) rename plugins/src/{permissions => common}/rule.js (100%) rename plugins/src/{collection => common}/table.js (100%) rename plugins/src/{permissions => common}/template.js (100%) rename plugins/src/{collection => common}/utils.js (100%) rename plugins/src/{permissions => common}/validator.js (100%) rename plugins/src/{common.js => common/writes.js} (100%) create mode 100644 plugins/src/fetch.js rename plugins/src/{reads => }/find.js (63%) rename plugins/src/{reads => }/findAll.js (64%) rename plugins/src/{reads => }/limit.js (60%) rename plugins/src/{reads => }/order.js (84%) rename plugins/src/{permissions/main.js => permissions.js} (100%) rename plugins/src/{permit_all/index.js => permit_all.js} (100%) delete mode 100644 plugins/src/reads/fetch.js delete mode 100644 plugins/src/reads/index.js delete mode 100644 plugins/src/reads/watch.js rename plugins/src/{timeout/index.js => timeout.js} (100%) create mode 100644 plugins/src/watch.js diff --git a/plugins/src/reads/above.js b/plugins/src/above.js similarity index 74% rename from plugins/src/reads/above.js rename to plugins/src/above.js index e84619349..9095cfb4f 100644 --- a/plugins/src/reads/above.js +++ b/plugins/src/above.js @@ -2,7 +2,7 @@ const isObject = require('./common').isObject; -module.exports = () => (req, res, next) => { +function above(req, res, next) { const args = req.options.above; if (args.length < 1 || args.length > 2) { next(new Error(`"above" expected 1 or 2 arguments but found ${args.length}.`)); @@ -14,4 +14,16 @@ module.exports = () => (req, res, next) => { req.setParameter({value: args[0], bound: args.length === 1 ? 'open' : args[1]}); next(); } -}; +} + +module.exports = (options) => ({ + name: 'hz_above', + activate: (ctx) => ({ + methods: { + above: { + type: 'option', + handler: above, + }, + }, + }), +}); diff --git a/plugins/src/reads/below.js b/plugins/src/below.js similarity index 74% rename from plugins/src/reads/below.js rename to plugins/src/below.js index 764a6795f..08890ec2b 100644 --- a/plugins/src/reads/below.js +++ b/plugins/src/below.js @@ -2,7 +2,7 @@ const isObject = require('./common').isObject; -module.exports = () => (req, res, next) => { +function below(req, res, next) { const args = req.options.below; if (args.length < 1 || args.length > 2) { next(new Error(`"below" expected 1 or 2 arguments but found ${args.length}.`)); @@ -14,4 +14,16 @@ module.exports = () => (req, res, next) => { req.setParameter({value: args[0], bound: args.length === 1 ? 'closed' : args[1]}); next(); } -}; +} + +module.exports = (options) => ({ + name: 'hz_below', + activate: (ctx) => ({ + methods: { + below: { + type: 'option', + handler: below, + }, + }, + }), +}); diff --git a/plugins/src/collection/main.js b/plugins/src/collection.js similarity index 100% rename from plugins/src/collection/main.js rename to plugins/src/collection.js diff --git a/plugins/src/collection/collection.js b/plugins/src/common/collection.js similarity index 100% rename from plugins/src/collection/collection.js rename to plugins/src/common/collection.js diff --git a/plugins/src/permissions/group.js b/plugins/src/common/group.js similarity index 100% rename from plugins/src/permissions/group.js rename to plugins/src/common/group.js diff --git a/plugins/src/collection/index.js b/plugins/src/common/index.js similarity index 100% rename from plugins/src/collection/index.js rename to plugins/src/common/index.js diff --git a/plugins/src/collection/metadata.js b/plugins/src/common/metadata.js similarity index 100% rename from plugins/src/collection/metadata.js rename to plugins/src/common/metadata.js diff --git a/plugins/src/reads/common.js b/plugins/src/common/reads.js similarity index 100% rename from plugins/src/reads/common.js rename to plugins/src/common/reads.js diff --git a/plugins/src/permissions/remake_error.js b/plugins/src/common/remake_error.js similarity index 100% rename from plugins/src/permissions/remake_error.js rename to plugins/src/common/remake_error.js diff --git a/plugins/src/permissions/rule.js b/plugins/src/common/rule.js similarity index 100% rename from plugins/src/permissions/rule.js rename to plugins/src/common/rule.js diff --git a/plugins/src/collection/table.js b/plugins/src/common/table.js similarity index 100% rename from plugins/src/collection/table.js rename to plugins/src/common/table.js diff --git a/plugins/src/permissions/template.js b/plugins/src/common/template.js similarity index 100% rename from plugins/src/permissions/template.js rename to plugins/src/common/template.js diff --git a/plugins/src/collection/utils.js b/plugins/src/common/utils.js similarity index 100% rename from plugins/src/collection/utils.js rename to plugins/src/common/utils.js diff --git a/plugins/src/permissions/validator.js b/plugins/src/common/validator.js similarity index 100% rename from plugins/src/permissions/validator.js rename to plugins/src/common/validator.js diff --git a/plugins/src/common.js b/plugins/src/common/writes.js similarity index 100% rename from plugins/src/common.js rename to plugins/src/common/writes.js diff --git a/plugins/src/fetch.js b/plugins/src/fetch.js new file mode 100644 index 000000000..a67117cf1 --- /dev/null +++ b/plugins/src/fetch.js @@ -0,0 +1,67 @@ +'use strict'; + +const common = require('./common'); + +function fetch(ctx) { + return (req, res, next) => { + const args = req.options.fetch; + const permissions = req.getParameter('hz_permissions'); + const conn = ctx.rdb_connection().connection(); + + if (args.length !== 0) { + next(new Error(`"fetch" expects 0 arguments but found ${args.length}`)); + } else if (!permissions) { + next(new Error('"fetch" requires permissions to run')); + } else { + common.make_reql(req).then((reql) => + reql.run(conn, common.reql_options) + ).then((result) => { + if (result !== null && result.constructor.name === 'Cursor') { + res.complete.then(() => { + result.close().catch(() => { }); + }); + + // TODO: reuse cursor batching + return result.eachAsync((item) => { + const validator = permissions(); + if (validator && !validator(req.clientCtx, item)) { + next(new Error('Operation not permitted.')); + result.close().catch(() => { }); + } else { + res.write([item]); + } + }).then(() => { + res.end(); + }); + } else { + const validator = permissions(); + if (result !== null && result.constructor.name === 'Array') { + for (const item of result) { + if (validator && !validator(req.clientCtx, item)) { + return next(new Error('Operation not permitted.')); + } + } + res.end(result); + } else if (validator && !validator(req.clientCtx, result)) { + next(new Error('Operation not permitted.')); + } else { + res.end([result]); + } + } + }).catch(next); + } + }; +} + +module.exports = () => ({ + name: 'hz_fetch', + activate: (ctx) => ({ + methods: { + fetch: { + type: 'terminal', + requires: ['hz_permissions'], + handler: fetch(ctx), + }, + }, + }), +}); diff --git a/plugins/src/reads/find.js b/plugins/src/find.js similarity index 63% rename from plugins/src/reads/find.js rename to plugins/src/find.js index 2611123b3..bb154a08b 100644 --- a/plugins/src/reads/find.js +++ b/plugins/src/find.js @@ -2,7 +2,7 @@ const isObject = require('./common').isObject; -module.exports = () => (req, res, next) => { +function find(req, res, next) { const args = req.options.find; if (args.length !== 1) { next(new Error(`"find" expected 1 argument but found ${args.length}.`)); @@ -12,4 +12,16 @@ module.exports = () => (req, res, next) => { req.setParameter(args[0]); next(); } -}; +} + +module.exports = () => ({ + name: 'hz_find', + activate: (ctx) => ({ + methods: { + find: { + type: 'option', + handler: find, + }, + }, + }), +}); diff --git a/plugins/src/reads/findAll.js b/plugins/src/findAll.js similarity index 64% rename from plugins/src/reads/findAll.js rename to plugins/src/findAll.js index 755b1d1ad..d1534a482 100644 --- a/plugins/src/reads/findAll.js +++ b/plugins/src/findAll.js @@ -2,7 +2,7 @@ const isObject = require('./common').isObject; -module.exports = () => (req, res, next) => { +function findAll(req, res, next) { const args = req.options.findAll; if (args.length < 1) { next(new Error(`"findAll" expected 1 or more arguments but found ${args.length}.`)); @@ -12,4 +12,16 @@ module.exports = () => (req, res, next) => { req.setParameter(args); next(); } -}; +} + +module.exports = () => ({ + name: 'hz_findAll', + activate: (ctx) => ({ + methods: { + findAll: { + type: 'option', + handler: findAll, + }, + }, + }), +}); diff --git a/plugins/src/index.js b/plugins/src/index.js index c8c50b3b6..02ba64dd6 100644 --- a/plugins/src/index.js +++ b/plugins/src/index.js @@ -25,13 +25,13 @@ const allPlugins = { }; -module.exports = function(options) { +module.exports = function (options) { options.methods = options.methods || Object.keys(allPlugins); const subplugins = options.methods.map((name) => { if (!allPlugins[name]) { throw new Error(`"${name}" is not a default Horizon method.`); } - return allPlugins[name]; + return allPlugins[name](options); }); return { @@ -44,7 +44,7 @@ module.exports = function(options) { deactivate: (ctx) => Promise.all(subplugins.map((p) => - Promise.resolve().then(() => p.deactivate(ctx)))), + Promise.resolve().then(() => p.deactivate && p.deactivate(ctx)))), }; }; diff --git a/plugins/src/reads/limit.js b/plugins/src/limit.js similarity index 60% rename from plugins/src/reads/limit.js rename to plugins/src/limit.js index daef98e86..5f7c5bd61 100644 --- a/plugins/src/reads/limit.js +++ b/plugins/src/limit.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = () => (req, res, next) => { +function limit(req, res, next) { const args = req.options.limit; if (args.length !== 1) { next(new Error(`"limit" expected 1 argument but found ${args.length}.`)); @@ -10,4 +10,16 @@ module.exports = () => (req, res, next) => { req.setParameter(args[0]); next(); } -}; +} + +module.exports = () => ({ + name: 'hz_limit', + activate: (ctx) => ({ + methods: { + limit: { + type: 'option', + handler: limit, + }, + }, + }), +}); diff --git a/plugins/src/reads/order.js b/plugins/src/order.js similarity index 84% rename from plugins/src/reads/order.js rename to plugins/src/order.js index dea80009f..4c52bf38a 100644 --- a/plugins/src/reads/order.js +++ b/plugins/src/order.js @@ -9,7 +9,7 @@ function convertField(value) { return typeof value === 'string' ? [value] : value; } -module.exports = () => (req, res, next) => { +function order(req, res, next) { const args = req.options.order; if (args.length < 1 || args.length > 2) { next(new Error(`"order" expected 1 or 2 arguments but found ${args.length}.`)); @@ -28,4 +28,16 @@ module.exports = () => (req, res, next) => { }); next(); } -}; +} + +module.exports = () => ({ + name: 'hz_order', + activate: (ctx) => ({ + methods: { + order: { + type: 'option', + handler: order, + }, + }, + }), +}); diff --git a/plugins/src/permissions/main.js b/plugins/src/permissions.js similarity index 100% rename from plugins/src/permissions/main.js rename to plugins/src/permissions.js diff --git a/plugins/src/permit_all/index.js b/plugins/src/permit_all.js similarity index 100% rename from plugins/src/permit_all/index.js rename to plugins/src/permit_all.js diff --git a/plugins/src/reads/fetch.js b/plugins/src/reads/fetch.js deleted file mode 100644 index 8651115d5..000000000 --- a/plugins/src/reads/fetch.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const common = require('./common'); - -module.exports = (server) => (req, res, next) => { - const args = req.options.fetch; - const permissions = req.getParameter('hz_permissions'); - const conn = server.rdb_connection().connection(); - - if (args.length !== 0) { - next(new Error(`"fetch" expects 0 arguments but found ${args.length}`)); - } else if (!permissions) { - next(new Error('"fetch" requires permissions to run')); - } else { - common.make_reql(req).then((reql) => - reql.run(conn, common.reql_options) - ).then((result) => { - if (result !== null && result.constructor.name === 'Cursor') { - res.complete.then(() => { - result.close().catch(() => { }); - }); - - // TODO: reuse cursor batching - return result.eachAsync((item) => { - const validator = permissions(); - if (validator && !validator(req.clientCtx, item)) { - next(new Error('Operation not permitted.')); - result.close().catch(() => { }); - } else { - res.write([item]); - } - }).then(() => { - res.end(); - }); - } else { - const validator = permissions(); - if (result !== null && result.constructor.name === 'Array') { - for (const item of result) { - if (validator && !validator(req.clientCtx, item)) { - return next(new Error('Operation not permitted.')); - } - } - res.end(result); - } else if (validator && !validator(req.clientCtx, result)) { - next(new Error('Operation not permitted.')); - } else { - res.end([result]); - } - } - }).catch(next); - } -}; diff --git a/plugins/src/reads/index.js b/plugins/src/reads/index.js deleted file mode 100644 index c4bd40273..000000000 --- a/plugins/src/reads/index.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const above = require('./above'); -const below = require('./below'); -const order = require('./order'); -const find = require('./find'); -const findAll = require('./findAll'); -const limit = require('./limit'); -const fetch = require('./fetch'); -const watch = require('./watch'); - -module.exports = (raw_config) => ({ - name: (raw_config && raw_config.name) || 'hz_reads', - activate: (server) => ({ - methods: { - above: { - type: 'option', - handler: above(server), - }, - below: { - type: 'option', - handler: below(server), - }, - order: { - type: 'option', - handler: order(server), - }, - find: { - type: 'option', - handler: find(server), - }, - findAll: { - type: 'option', - handler: findAll(server), - }, - limit: { - type: 'option', - handler: limit(server), - }, - fetch: { - requires: ['hz_permissions'], - type: 'terminal', - handler: fetch(server), - }, - watch: { - requires: ['hz_permissions'], - type: 'terminal', - handler: watch(server), - }, - }, - }), -}); diff --git a/plugins/src/reads/watch.js b/plugins/src/reads/watch.js deleted file mode 100644 index 8c28a198d..000000000 --- a/plugins/src/reads/watch.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const common = require('./common'); - -module.exports = (server) => (req, res, next) => { - const args = req.options.watch; - const permissions = req.getParameter('hz_permissions'); - const conn = server.rdb_connection().connection(); - - if (args.length !== 0) { - next(new Error(`"watch" expects 0 arguments but found ${args.length}`)); - } else if (!permissions) { - next(new Error('"watch" requires permissions to run')); - } else { - common.make_reql(req).then((reql) => - reql.changes({ - include_initial: true, - include_states: true, - include_types: true, - include_offsets: - req.getParameter('order') !== undefined && - req.getParameter('limit') !== undefined, - }).run(conn, common.reql_options) - ).then((feed) => { - res.complete.then(() => { - feed.close().catch(() => { }); - }); - - // TODO: reuse cursor batches - return feed.eachAsync((item) => { - if (item.state === 'initializing') { - // Do nothing - we don't care - } else if (item.state === 'ready') { - res.write([], 'synced'); - } else { - const validator = permissions(); - if (validator) { - if ((item.old_val && !validator(req.clientCtx, item.old_val)) || - (item.new_val && !validator(req.clientCtx, item.new_val))) { - next(new Error('Operation not permitted.')); - } - } - res.write([item]); - } - }); - }).then(() => res.end()).catch(next); - } -}; diff --git a/plugins/src/timeout/index.js b/plugins/src/timeout.js similarity index 100% rename from plugins/src/timeout/index.js rename to plugins/src/timeout.js diff --git a/plugins/src/watch.js b/plugins/src/watch.js new file mode 100644 index 000000000..2a8d8a074 --- /dev/null +++ b/plugins/src/watch.js @@ -0,0 +1,63 @@ +'use strict'; + +const common = require('./common'); + +function watch(server) { + return (req, res, next) => { + const args = req.options.watch; + const permissions = req.getParameter('hz_permissions'); + const conn = server.rdb_connection().connection(); + + if (args.length !== 0) { + next(new Error(`"watch" expects 0 arguments but found ${args.length}`)); + } else if (!permissions) { + next(new Error('"watch" requires permissions to run')); + } else { + common.make_reql(req).then((reql) => + reql.changes({ + include_initial: true, + include_states: true, + include_types: true, + include_offsets: + req.getParameter('order') !== undefined && + req.getParameter('limit') !== undefined, + }).run(conn, common.reql_options) + ).then((feed) => { + res.complete.then(() => { + feed.close().catch(() => { }); + }); + + // TODO: reuse cursor batches + return feed.eachAsync((item) => { + if (item.state === 'initializing') { + // Do nothing - we don't care + } else if (item.state === 'ready') { + res.write([], 'synced'); + } else { + const validator = permissions(); + if (validator) { + if ((item.old_val && !validator(req.clientCtx, item.old_val)) || + (item.new_val && !validator(req.clientCtx, item.new_val))) { + next(new Error('Operation not permitted.')); + } + } + res.write([item]); + } + }); + }).then(() => res.end()).catch(next); + } + }; +} + +module.exports = () => ({ + name: 'hz_watch', + activate: (ctx) => ({ + methods: { + watch: { + type: 'terminal', + requires: ['hz_permissions'], + handler: watch(ctx), + }, + }, + }), +}); From 032554a0ada88438e1e9fac7b30dff5c0c8d31c3 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 03:36:24 -0700 Subject: [PATCH 49/86] more plugin reorganization --- plugins/src/above.js | 2 +- plugins/src/below.js | 2 +- plugins/src/collection.js | 10 +- .../src/{common => collection}/collection.js | 0 plugins/src/{common => collection}/index.js | 0 .../src/{common => collection}/metadata.js | 8 +- plugins/src/{common => collection}/table.js | 0 plugins/src/common/reads.js | 11 +- plugins/src/common/remake_error.js | 11 -- plugins/src/common/utils.js | 31 ++++- plugins/src/common/writes.js | 14 +- plugins/src/fetch.js | 5 +- plugins/src/find.js | 2 +- plugins/src/findAll.js | 2 +- plugins/src/index.js | 30 +---- plugins/src/insert.js | 66 +++++---- plugins/src/permissions.js | 3 +- plugins/src/{common => permissions}/group.js | 0 plugins/src/{common => permissions}/rule.js | 0 .../src/{common => permissions}/template.js | 0 .../src/{common => permissions}/validator.js | 0 plugins/src/permit_all.js | 3 +- plugins/src/remove.js | 116 +++++++++------- plugins/src/replace.js | 98 ++++++++------ plugins/src/store.js | 120 +++++++++-------- plugins/src/timeout.js | 26 ++-- plugins/src/update.js | 108 ++++++++------- plugins/src/upsert.js | 126 ++++++++++-------- plugins/src/watch.js | 5 +- plugins/src/writes/index.js | 46 ------- 30 files changed, 431 insertions(+), 414 deletions(-) rename plugins/src/{common => collection}/collection.js (100%) rename plugins/src/{common => collection}/index.js (100%) rename plugins/src/{common => collection}/metadata.js (98%) rename plugins/src/{common => collection}/table.js (100%) delete mode 100644 plugins/src/common/remake_error.js rename plugins/src/{common => permissions}/group.js (100%) rename plugins/src/{common => permissions}/rule.js (100%) rename plugins/src/{common => permissions}/template.js (100%) rename plugins/src/{common => permissions}/validator.js (100%) delete mode 100644 plugins/src/writes/index.js diff --git a/plugins/src/above.js b/plugins/src/above.js index 9095cfb4f..07bc63d93 100644 --- a/plugins/src/above.js +++ b/plugins/src/above.js @@ -1,6 +1,6 @@ 'use strict'; -const isObject = require('./common').isObject; +const {isObject} = require('./common/utils'); function above(req, res, next) { const args = req.options.above; diff --git a/plugins/src/below.js b/plugins/src/below.js index 08890ec2b..a9a7a9d46 100644 --- a/plugins/src/below.js +++ b/plugins/src/below.js @@ -1,6 +1,6 @@ 'use strict'; -const isObject = require('./common').isObject; +const {isObject} = require('./common/utils'); function below(req, res, next) { const args = req.options.below; diff --git a/plugins/src/collection.js b/plugins/src/collection.js index 1e26e0100..64c33c4dc 100644 --- a/plugins/src/collection.js +++ b/plugins/src/collection.js @@ -1,6 +1,6 @@ 'use strict'; -import {ReliableMetadata} from './metadata.js'; +const {ReliableMetadata} = require('./common/metadata.js'); function collection(server, metadata) { return (req, res, next) => { @@ -19,11 +19,9 @@ function collection(server, metadata) { }; } -export default function(raw_config) { +module.exports = (options) => ({ return { - name: (raw_config && raw_config.name) || 'collection', - // RSI: make sure we check the arity and become ready if people - // don't take the callbacks. + name: 'hz_collection', activate: (server, onReady, onUnready) => { const metadata = new ReliableMetadata( server, @@ -44,4 +42,4 @@ export default function(raw_config) { }; }, }; -} +}); diff --git a/plugins/src/common/collection.js b/plugins/src/collection/collection.js similarity index 100% rename from plugins/src/common/collection.js rename to plugins/src/collection/collection.js diff --git a/plugins/src/common/index.js b/plugins/src/collection/index.js similarity index 100% rename from plugins/src/common/index.js rename to plugins/src/collection/index.js diff --git a/plugins/src/common/metadata.js b/plugins/src/collection/metadata.js similarity index 98% rename from plugins/src/common/metadata.js rename to plugins/src/collection/metadata.js index db3602a83..b6c875298 100644 --- a/plugins/src/common/metadata.js +++ b/plugins/src/collection/metadata.js @@ -1,12 +1,12 @@ 'use strict'; -import {r, logger, Reliable, ReliableUnion} from '@horizon/server'; const Collection = require('./collection').Collection; const utils = require('./utils'); const assert = require('assert'); -const version_field = '$hz_v$'; +import {r, logger, Reliable, ReliableUnion} from '@horizon/server'; + const metadata_version = [2, 0, 0]; export const create_collection = (db, name, conn) => @@ -73,7 +73,7 @@ class ReliableInit extends Reliable { this.check_attempt(attempt); logger.debug('checking rethinkdb version'); const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version'); - return q.run(conn).then((res) => utils.rethinkdb_version_check(res)); + return q.run(conn).then((res) => utils.rethinkdbVersionCheck(res)); }).then(() => { this.check_attempt(attempt); logger.debug('checking for old metadata version'); @@ -114,7 +114,6 @@ class ReliableInit extends Reliable { { id: 'admin', groups: ['admin'], - [version_field]: 0, }, old_row), {returnChanges: 'always'})('changes')(0) @@ -128,7 +127,6 @@ class ReliableInit extends Reliable { { id: 'admin', rules: {carte_blanche: {template: 'any()'}}, - [version_field]: 0, }, old_row), {returnChanges: 'always'})('changes')(0) diff --git a/plugins/src/common/table.js b/plugins/src/collection/table.js similarity index 100% rename from plugins/src/common/table.js rename to plugins/src/collection/table.js diff --git a/plugins/src/common/reads.js b/plugins/src/common/reads.js index 9312fc990..7550da70c 100644 --- a/plugins/src/common/reads.js +++ b/plugins/src/common/reads.js @@ -2,15 +2,6 @@ const {r} = require('@horizon/server'); -const reql_options = { - timeFormat: 'raw', - binaryFormat: 'raw', -}; - -function isObject(x) { - return !Array.isArray(x) && x !== null; -} - function object_to_fields(obj) { return Object.keys(obj).map((key) => { const value = obj[key]; @@ -129,4 +120,4 @@ const make_reql = (req) => Promise.resolve().then(() => { return reqlPromise.then((reql) => (limit !== undefined ? reql.limit(limit) : reql)); }); -module.exports = {make_reql, isObject, reql_options}; +module.exports = {make_reql, reql_options}; diff --git a/plugins/src/common/remake_error.js b/plugins/src/common/remake_error.js deleted file mode 100644 index 9ec5ec5ef..000000000 --- a/plugins/src/common/remake_error.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -// Used when evaluating things in a different VM context - the errors -// thrown from there will not evaluate as `instanceof Error`, so we recreate them. -const remake_error = (err) => { - const new_err = new Error(err.message || 'Unknown error when evaluating template.'); - new_err.stack = err.stack || new_err.stack; - throw new_err; -}; - -module.exports = remake_error; diff --git a/plugins/src/common/utils.js b/plugins/src/common/utils.js index 1b79966f1..211009628 100644 --- a/plugins/src/common/utils.js +++ b/plugins/src/common/utils.js @@ -4,7 +4,7 @@ const MIN_VERSION = [2, 3, 1]; // Recursive version compare, could be flatter but opted for instant return if // comparison is greater rather than continuing to compare to end. -const version_compare = (actual, minimum) => { +function versionCompare(actual, minimum) { for (let i = 0; i < minimum.length; ++i) { if (actual[i] > minimum[i]) { return true; @@ -13,10 +13,10 @@ const version_compare = (actual, minimum) => { } } return true; -}; +} // Check that RethinkDB matches version requirements -const rethinkdb_version_check = (version_string) => { +function rethinkdbVersionCheck(version_string) { const rethinkdb_version_regex = /^rethinkdb (\d+)\.(\d+)\.(\d+)/i; const matches = rethinkdb_version_regex.exec(version_string); @@ -32,8 +32,31 @@ const rethinkdb_version_check = (version_string) => { throw new Error('Unable to determine RethinkDB version, check ' + `RethinkDB is >= ${MIN_VERSION.join('.')}.`); } +} + +// Used when evaluating things in a different VM context - the errors +// thrown from there will not evaluate as `instanceof Error`, so we recreate them. +function remakeError(err) { + const new_err = new Error(err.message || 'Unknown error when evaluating template.'); + new_err.stack = err.stack || new_err.stack; + throw new_err; +} + +function isObject(x) { + return !Array.isArray(x) && x !== null; +} + +const reqlOptions = { + timeFormat: 'raw', + binaryFormat: 'raw', }; +const versionField = '$hz_v$'; + module.exports = { - rethinkdb_version_check, + rethinkdbVersionCheck, + remakeError, + isObject, + reqlOptions, + versionField, }; diff --git a/plugins/src/common/writes.js b/plugins/src/common/writes.js index 266170412..9d7f97b68 100644 --- a/plugins/src/common/writes.js +++ b/plugins/src/common/writes.js @@ -1,22 +1,19 @@ 'use strict'; +const utils = require('./utils'); + const assert = require('assert'); const {r} = require('@horizon/server'); -// Common functionality used by write requests - -const reql_options = { - timeFormat: 'raw', - binaryFormat: 'raw', -}; +const hz_v = utils.version_field; +// Common functionality used by write requests const invalidated_msg = 'Write invalidated by another request, try again.'; const missing_msg = 'The document was missing.'; const timeout_msg = 'Operation timed out.'; const unauthorized_msg = 'Operation not permitted.'; -const hz_v = '$hz_v$'; function apply_version(row, new_version) { row.merge(r.object(hz_v, new_version)); } @@ -188,11 +185,8 @@ module.exports = { missing_msg, timeout_msg, unauthorized_msg, - make_write_response, - version_field: hz_v, apply_version, retry_loop, validate_old_row_required, validate_old_row_optional, - reql_options, }; diff --git a/plugins/src/fetch.js b/plugins/src/fetch.js index a67117cf1..336580ba5 100644 --- a/plugins/src/fetch.js +++ b/plugins/src/fetch.js @@ -1,6 +1,7 @@ 'use strict'; -const common = require('./common'); +const utils = require('./common/utils'); +const common = require('./common/reads'); function fetch(ctx) { return (req, res, next) => { @@ -14,7 +15,7 @@ function fetch(ctx) { next(new Error('"fetch" requires permissions to run')); } else { common.make_reql(req).then((reql) => - reql.run(conn, common.reql_options) + reql.run(conn, utils.reqlOptions) ).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { res.complete.then(() => { diff --git a/plugins/src/find.js b/plugins/src/find.js index bb154a08b..51bb0ee76 100644 --- a/plugins/src/find.js +++ b/plugins/src/find.js @@ -1,6 +1,6 @@ 'use strict'; -const isObject = require('./common').isObject; +const {isObject} = require('./common/utils'); function find(req, res, next) { const args = req.options.find; diff --git a/plugins/src/findAll.js b/plugins/src/findAll.js index d1534a482..680006bc9 100644 --- a/plugins/src/findAll.js +++ b/plugins/src/findAll.js @@ -1,6 +1,6 @@ 'use strict'; -const isObject = require('./common').isObject; +const {isObject} = require('./common/utils'); function findAll(req, res, next) { const args = req.options.findAll; diff --git a/plugins/src/index.js b/plugins/src/index.js index 02ba64dd6..be22679ae 100644 --- a/plugins/src/index.js +++ b/plugins/src/index.js @@ -1,6 +1,6 @@ 'use strict'; -const allPlugins = { +module.exports = { // Collections API above: require('./above'), below: require('./below'), @@ -23,31 +23,3 @@ const allPlugins = { permissions: require('./permissions'), permit_all: require('./permit_all'), }; - - -module.exports = function (options) { - options.methods = options.methods || Object.keys(allPlugins); - const subplugins = options.methods.map((name) => { - if (!allPlugins[name]) { - throw new Error(`"${name}" is not a default Horizon method.`); - } - return allPlugins[name](options); - }); - - return { - name: 'hz_default_plugins', - activate: (ctx) => - Promise.all(subplugins.map((p) => - Promise.resolve().then(() => p.activate(ctx)))).then((results) => ({ - methods: Object.assign({}, results.methods), - })), - - deactivate: (ctx) => - Promise.all(subplugins.map((p) => - Promise.resolve().then(() => p.deactivate && p.deactivate(ctx)))), - }; -}; - -for (const name of allPlugins) { - module.exports[name] = allPlugins[name]; -} diff --git a/plugins/src/insert.js b/plugins/src/insert.js index 949c097fa..ef351a358 100644 --- a/plugins/src/insert.js +++ b/plugins/src/insert.js @@ -1,33 +1,49 @@ 'use strict'; -const common = require('./common'); +const utils = require('./common/utils'); +const common = require('./common/writes'); const {r} = require('@horizon/server'); -module.exports = (server) => (request, response, next) => { - const conn = server.rdb_connection().connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); +function insert(server) { + return (request, response, next) => { + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('hz_permissions'); - if (!collection) { - throw new Error('No collection given for insert operation.'); - } else if (!permissions) { - throw new Error('No permissions given for insert operation.'); - } + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } - common.retry_loop(request.options.insert, permissions, timeout, - (rows) => // pre-validation, all rows - Array(rows.length).fill(null), - (validator, row, info) => { // validation, each row - if (!validator(request.clientCtx, info, row)) { - return new Error(common.unauthorized_msg); - } + common.retry_loop(request.options.insert, permissions, timeout, + (rows) => // pre-validation, all rows + Array(rows.length).fill(null), + (validator, row, info) => { // validation, each row + if (!validator(request.clientCtx, info, row)) { + return new Error(common.unauthorized_msg); + } + }, + (rows) => // write to database, all valid rows + collection.table + .insert(rows.map((row) => common.apply_version(r.expr(row), 0)), + {returnChanges: 'always'}) + .run(conn, utils.reqlOptions) + ).then((msg) => response.end(msg)).catch(next); + }; +} + +module.exports = () => ({ + name: 'hz_insert', + activate: (ctx) => ({ + methods: { + insert: { + type: 'terminal', + requires: ['hz_permissions'], + handler: insert(ctx), + }, }, - (rows) => // write to database, all valid rows - collection.table - .insert(rows.map((row) => common.apply_version(r.expr(row), 0)), - {returnChanges: 'always'}) - .run(conn, common.reql_options) - ).then((msg) => response.end(msg)).catch(next); -}; + }), +}); diff --git a/plugins/src/permissions.js b/plugins/src/permissions.js index 94a4fbde4..5ad44e247 100644 --- a/plugins/src/permissions.js +++ b/plugins/src/permissions.js @@ -302,7 +302,6 @@ module.exports = (raw_config) => { if (config.cacheTimeout === undefined) { config.cacheTimeout = 5000; } - config.name = config.name || 'permissions'; config.usersTable = config.usersTable || 'users'; config.groupsTable = config.groupsTable || 'hz_groups'; @@ -312,7 +311,7 @@ module.exports = (raw_config) => { let authCb; let disconnectCb; return { - name, + name: 'hz_permissions', activate(ctx) { logger.info('Activating permissions plugin.'); diff --git a/plugins/src/common/group.js b/plugins/src/permissions/group.js similarity index 100% rename from plugins/src/common/group.js rename to plugins/src/permissions/group.js diff --git a/plugins/src/common/rule.js b/plugins/src/permissions/rule.js similarity index 100% rename from plugins/src/common/rule.js rename to plugins/src/permissions/rule.js diff --git a/plugins/src/common/template.js b/plugins/src/permissions/template.js similarity index 100% rename from plugins/src/common/template.js rename to plugins/src/permissions/template.js diff --git a/plugins/src/common/validator.js b/plugins/src/permissions/validator.js similarity index 100% rename from plugins/src/common/validator.js rename to plugins/src/permissions/validator.js diff --git a/plugins/src/permit_all.js b/plugins/src/permit_all.js index cce6a7de1..51c73d255 100644 --- a/plugins/src/permit_all.js +++ b/plugins/src/permit_all.js @@ -1,8 +1,7 @@ 'use strict'; module.exports = (raw_config) => ({ - name: (raw_config && raw_config.name) || 'permissions', - + name: 'hz_permissions', activate: () => ({ methods: { hz_permissions: { diff --git a/plugins/src/remove.js b/plugins/src/remove.js index f66472476..d0e41dc99 100644 --- a/plugins/src/remove.js +++ b/plugins/src/remove.js @@ -1,55 +1,71 @@ 'use strict'; -const common = require('./common'); -const hz_v = common.version_field; +const utils = require('./common/utils'); +const common = require('./common/writes'); +const hz_v = utils.versionField; const {r} = require('@horizon/server'); -module.exports = (server) => (request, response, next) => { - const conn = server.rdb_connection().connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); - - if (!collection) { - throw new Error('No collection given for insert operation.'); - } else if (!permissions) { - throw new Error('No permissions given for insert operation.'); - } - - common.retry_loop(request.options.remove, permissions, timeout, - (rows) => // pre-validation, all rows - r.expr(rows.map((row) => row.id)) - .map((id) => collection.table.get(id)) - .run(conn, common.reql_options), - (validator, row, info) => - common.validate_old_row_required(validator, request.clientCtx, row, info, null), - (rows) => // write to database, all valid rows - r.expr(rows).do((row_data) => - row_data.forEach((info) => - collection.table.get(info('id')).replace((row) => - r.branch(// The row may have been deleted between the get and now - row.eq(null), - null, - - // The row may have been changed between the get and now - r.and(info.hasFields(hz_v), - row(hz_v).default(-1).ne(info(hz_v))), - r.error(common.invalidated_msg), - - // Otherwise, we can safely remove the row - null), - - {returnChanges: 'always'})) - // Pretend like we deleted rows that didn't exist - .do((res) => - res.merge({changes: - r.range(row_data.count()).map((index) => - r.branch(res('changes')(index)('old_val').eq(null), - res('changes')(index).merge( - {old_val: {id: row_data(index)('id')}}), - res('changes')(index))).coerceTo('array'), - }))) - .run(conn, common.reql_options) - ).then((msg) => response.end(msg)).catch(next); -}; +function remove(server) { + return (request, response, next) => { + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('hz_permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.remove, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows.map((row) => row.id)) + .map((id) => collection.table.get(id)) + .run(conn, utils.reqlOptions), + (validator, row, info) => + common.validate_old_row_required(validator, request.clientCtx, row, info, null), + (rows) => // write to database, all valid rows + r.expr(rows).do((row_data) => + row_data.forEach((info) => + collection.table.get(info('id')).replace((row) => + r.branch(// The row may have been deleted between the get and now + row.eq(null), + null, + + // The row may have been changed between the get and now + r.and(info.hasFields(hz_v), + row(hz_v).default(-1).ne(info(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can safely remove the row + null), + + {returnChanges: 'always'})) + // Pretend like we deleted rows that didn't exist + .do((res) => + res.merge({changes: + r.range(row_data.count()).map((index) => + r.branch(res('changes')(index)('old_val').eq(null), + res('changes')(index).merge( + {old_val: {id: row_data(index)('id')}}), + res('changes')(index))).coerceTo('array'), + }))) + .run(conn, utils.reqlOptions) + ).then((msg) => response.end(msg)).catch(next); + }; +} + +module.exports = () => ({ + name: 'hz_remove', + activate: (ctx) => ({ + methods: { + remove: { + type: 'terminal', + requires: ['hz_permissions'], + handler: remove(ctx), + }, + }, + }), +}); diff --git a/plugins/src/replace.js b/plugins/src/replace.js index 3052e06a7..2ea5b2d19 100644 --- a/plugins/src/replace.js +++ b/plugins/src/replace.js @@ -1,46 +1,62 @@ 'use strict'; -const common = require('./common'); -const hz_v = common.version_field; +const utils = require('./common/utils'); +const common = require('./common/writes'); +const hz_v = utils.versionField; const {r} = require('@horizon/server'); -module.exports = (server) => (request, response, next) => { - const conn = server.rdb_connection().connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); - - if (!collection) { - throw new Error('No collection given for insert operation.'); - } else if (!permissions) { - throw new Error('No permissions given for insert operation.'); - } - - common.retry_loop(request.options.replace, permissions, timeout, - (rows) => // pre-validation, all rows - r.expr(rows.map((row) => row.id)) - .map((id) => collection.table.get(id)) - .run(conn, common.reql_options), - (validator, row, info) => - common.validate_old_row_required(validator, request.clientCtx, row, info, row), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - collection.table.get(new_row('id')).replace((old_row) => - r.branch(// The row may have been deleted between the get and now - old_row.eq(null), - r.error(common.missing_msg), - - // The row may have been changed between the get and now - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), - - // Otherwise, we can safely replace the row - common.apply_version( - new_row, old_row(hz_v).default(-1).add(1))), - {returnChanges: 'always'})) - .run(conn, common.reql_options) - ).then((msg) => response.end(msg)).catch(next); -}; +function replace(server) { + return (request, response, next) => { + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('hz_permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.replace, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows.map((row) => row.id)) + .map((id) => collection.table.get(id)) + .run(conn, utils.reqlOptions), + (validator, row, info) => + common.validate_old_row_required(validator, request.clientCtx, row, info, row), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + collection.table.get(new_row('id')).replace((old_row) => + r.branch(// The row may have been deleted between the get and now + old_row.eq(null), + r.error(common.missing_msg), + + // The row may have been changed between the get and now + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can safely replace the row + common.apply_version( + new_row, old_row(hz_v).default(-1).add(1))), + {returnChanges: 'always'})) + .run(conn, utils.reqlOptions) + ).then((msg) => response.end(msg)).catch(next); + }; +} + +module.exports = () => ({ + name: 'hz_replace', + activate: (ctx) => ({ + methods: { + replace: { + type: 'terminal', + requires: ['hz_permissions'], + handler: replace(ctx), + }, + }, + }), +}); diff --git a/plugins/src/store.js b/plugins/src/store.js index 2edc59a60..cf5916bb2 100644 --- a/plugins/src/store.js +++ b/plugins/src/store.js @@ -1,59 +1,75 @@ 'use strict'; -const common = require('./common'); -const hz_v = common.version_field; +const utils = require('./common/utils'); +const common = require('./common/writes'); +const hz_v = utils.versionField; const {r} = require('@horizon/server'); -module.exports = (server) => (request, response, next) => { - const conn = server.rdb_connection().connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); - - if (!collection) { - throw new Error('No collection given for insert operation.'); - } else if (!permissions) { - throw new Error('No permissions given for insert operation.'); - } - - common.retry_loop(request.options.store, permissions, timeout, - (rows) => // pre-validation, all rows - r.expr(rows.map((row) => (row.id === undefined ? null : row.id))) - .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) - .run(conn, common.reql_options), - (validator, row, info) => - common.validate_old_row_optional(validator, request.clientCtx, row, info, row), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).replace((old_row) => - r.branch( - old_row.eq(null), - r.branch( - // Error if we were expecting the row to exist - new_row.hasFields(hz_v), - r.error(common.invalidated_msg), +function store(server) { + return (request, response, next) => { + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('hz_permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } - // Otherwise, insert the row - common.apply_version(new_row, 0) - ), + common.retry_loop(request.options.store, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows.map((row) => (row.id === undefined ? null : row.id))) + .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) + .run(conn, utils.reqlOptions), + (validator, row, info) => + common.validate_old_row_optional(validator, request.clientCtx, row, info, row), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + r.branch(new_row.hasFields('id'), + collection.table.get(new_row('id')).replace((old_row) => r.branch( - // The row may have changed from the expected version - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), - - // Otherwise, we can overwrite the row - common.apply_version(new_row, - old_row(hz_v).default(-1).add(1)) - ) - ), {returnChanges: 'always'}), - - // The new row does not have an id, so we insert it with an autogen id - collection.table.insert(common.apply_version(new_row, 0), - {returnChanges: 'always'}))) - .run(conn, common.reql_options) - ).then((msg) => response.end(msg)).catch(next); -}; + old_row.eq(null), + r.branch( + // Error if we were expecting the row to exist + new_row.hasFields(hz_v), + r.error(common.invalidated_msg), + + // Otherwise, insert the row + common.apply_version(new_row, 0) + ), + r.branch( + // The row may have changed from the expected version + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can overwrite the row + common.apply_version(new_row, + old_row(hz_v).default(-1).add(1)) + ) + ), {returnChanges: 'always'}), + + // The new row does not have an id, so we insert it with an autogen id + collection.table.insert(common.apply_version(new_row, 0), + {returnChanges: 'always'}))) + .run(conn, utils.reqlOptions) + ).then((msg) => response.end(msg)).catch(next); + }; +} + +module.exports = () => ({ + name: 'hz_store', + activate: (ctx) => ({ + methods: { + store: { + type: 'terminal', + requires: ['hz_permissions'], + handler: store(ctx), + }, + }, + }), +}); diff --git a/plugins/src/timeout.js b/plugins/src/timeout.js index 67b6b3437..54b8ef46b 100644 --- a/plugins/src/timeout.js +++ b/plugins/src/timeout.js @@ -1,22 +1,24 @@ 'use strict'; +function timeout(req, res, next) { + const args = req.options.timeout; + if (args.length !== 1) { + next(new Error(`"timeout" expected 1 argument but found ${args.length}`)); + } else if (typeof args[0] !== 'number') { + next(new Error('timeout must be a number')); + } else { + req.setParameter(new Date() + args[0]); + next(); + } +} + module.exports = (raw_config) => ({ - name: (raw_config && raw_config.name) || 'hz_timeout', + name: 'hz_timeout', activate: () => ({ methods: ({ timeout: { type: 'option', - handler: (req, res, next) => { - const args = req.options.timeout; - if (args.length !== 1) { - next(new Error(`"timeout" expected 1 argument but found ${args.length}`)); - } else if (typeof args[0] !== 'number') { - next(new Error('timeout must be a number')); - } else { - req.setParameter(new Date() + args[0]); - next(); - } - }, + handler: timeout, }, }), }), diff --git a/plugins/src/update.js b/plugins/src/update.js index 71c4c6a62..e0954f274 100644 --- a/plugins/src/update.js +++ b/plugins/src/update.js @@ -1,51 +1,67 @@ 'use strict'; -const common = require('./common'); -const hz_v = common.version_field; +const utils = require('./common/utils'); +const common = require('./common/writes'); +const hz_v = utils.versionField; const {r} = require('@horizon/server'); -module.exports = (server) => (request, response, next) => { - const conn = server.rdb_connection().connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); - - if (!collection) { - throw new Error('No collection given for insert operation.'); - } else if (!permissions) { - throw new Error('No permissions given for insert operation.'); - } - - common.retry_loop(request.options.update, permissions, timeout, - (rows) => // pre-validation, all rows - r.expr(rows) - .map((new_row) => - collection.table.get(new_row('id')).do((old_row) => - r.branch(old_row.eq(null), - null, - [old_row, old_row.merge(new_row)]))) - .run(conn, common.reql_options), - (validator, row, info) => - common.validate_old_row_required( - validator, request.clientCtx, row, info[0], info[1]), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - collection.table.get(new_row('id')).replace((old_row) => - r.branch(// The row may have been deleted between the get and now - old_row.eq(null), - r.error(common.missing_msg), - - // The row may have been changed between the get and now - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), - - // Otherwise we can update the row and increment the version - common.apply_version(old_row.merge(new_row), - old_row(hz_v).default(-1).add(1))), - {returnChanges: 'always'})) - .run(conn, common.reql_options) - ).then((msg) => response.end(msg)).catch(next); -}; +function update(server) { + return (request, response, next) => { + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('hz_permissions'); + + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } + + common.retry_loop(request.options.update, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows) + .map((new_row) => + collection.table.get(new_row('id')).do((old_row) => + r.branch(old_row.eq(null), + null, + [old_row, old_row.merge(new_row)]))) + .run(conn, utils.reqlOptions), + (validator, row, info) => + common.validate_old_row_required( + validator, request.clientCtx, row, info[0], info[1]), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + collection.table.get(new_row('id')).replace((old_row) => + r.branch(// The row may have been deleted between the get and now + old_row.eq(null), + r.error(common.missing_msg), + + // The row may have been changed between the get and now + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise we can update the row and increment the version + common.apply_version(old_row.merge(new_row), + old_row(hz_v).default(-1).add(1))), + {returnChanges: 'always'})) + .run(conn, utils.reqlOptions) + ).then((msg) => response.end(msg)).catch(next); + }; +} + +module.exports = () => ({ + name: 'hz_update', + activate: (ctx) => ({ + methods: { + update: { + type: 'terminal', + requires: ['hz_permissions'], + handler: update(ctx), + }, + }, + }), +}); diff --git a/plugins/src/upsert.js b/plugins/src/upsert.js index 8bf45e2f8..74e982aee 100644 --- a/plugins/src/upsert.js +++ b/plugins/src/upsert.js @@ -1,66 +1,82 @@ 'use strict'; -const common = require('./common'); -const hz_v = common.version_field; +const utils = require('./common/utils'); +const common = require('./common/writes'); +const hz_v = utils.versionField; const {r} = require('@horizon/server'); -module.exports = (server) => (request, response, next) => { - const conn = server.rdb_connection().connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); +function upsert(server) { + return (request, response, next) => { + const conn = server.rdb_connection().connection(); + const timeout = request.getParameter('timeout'); + const collection = request.getParameter('collection'); + const permissions = request.getParameter('hz_permissions'); - if (!collection) { - throw new Error('No collection given for insert operation.'); - } else if (!permissions) { - throw new Error('No permissions given for insert operation.'); - } + if (!collection) { + throw new Error('No collection given for insert operation.'); + } else if (!permissions) { + throw new Error('No permissions given for insert operation.'); + } - common.retry_loop(request.options.upsert, permissions, timeout, - (rows) => // pre-validation, all rows - r.expr(rows) - .map((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).do((old_row) => - r.branch(old_row.eq(null), - [null, new_row], - [old_row, old_row.merge(new_row)])), - [null, new_row])) - .run(conn, common.reql_options), - (validator, row, info) => - common.validate_old_row_optional( - validator, request.clientCtx, row, info[0], info[1]), - (rows) => // write to database, all valid rows - r.expr(rows) - .forEach((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).replace((old_row) => - r.branch( - old_row.eq(null), + common.retry_loop(request.options.upsert, permissions, timeout, + (rows) => // pre-validation, all rows + r.expr(rows) + .map((new_row) => + r.branch(new_row.hasFields('id'), + collection.table.get(new_row('id')).do((old_row) => + r.branch(old_row.eq(null), + [null, new_row], + [old_row, old_row.merge(new_row)])), + [null, new_row])) + .run(conn, utils.reqlOptions), + (validator, row, info) => + common.validate_old_row_optional( + validator, request.clientCtx, row, info[0], info[1]), + (rows) => // write to database, all valid rows + r.expr(rows) + .forEach((new_row) => + r.branch(new_row.hasFields('id'), + collection.table.get(new_row('id')).replace((old_row) => r.branch( - // Error if we were expecting the row to exist - new_row.hasFields(hz_v), - r.error(common.invalidated_msg), + old_row.eq(null), + r.branch( + // Error if we were expecting the row to exist + new_row.hasFields(hz_v), + r.error(common.invalidated_msg), - // Otherwise, insert the row - common.apply_version(new_row, 0) - ), - r.branch( - // The row may have changed from the expected version - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), + // Otherwise, insert the row + common.apply_version(new_row, 0) + ), + r.branch( + // The row may have changed from the expected version + r.and(new_row.hasFields(hz_v), + old_row(hz_v).default(-1).ne(new_row(hz_v))), + r.error(common.invalidated_msg), + + // Otherwise, we can update the row and increment the version + common.apply_version(old_row.merge(new_row), + old_row(hz_v).default(-1).add(1)) + ) + ), {returnChanges: 'always'}), - // Otherwise, we can update the row and increment the version - common.apply_version(old_row.merge(new_row), - old_row(hz_v).default(-1).add(1)) - ) - ), {returnChanges: 'always'}), + // The new row did not have an id, so we insert it with an autogen id + collection.table.insert(common.apply_version(new_row, 0), + {returnChanges: 'always'}))) + .run(conn, utils.reqlOptions) + ).then((msg) => response.end(msg)).catch(next); + }; +} - // The new row did not have an id, so we insert it with an autogen id - collection.table.insert(common.apply_version(new_row, 0), - {returnChanges: 'always'}))) - .run(conn, common.reql_options) - ).then((msg) => response.end(msg)).catch(next); -}; +module.exports = () => ({ + name: 'hz_upsert', + activate: (ctx) => ({ + methods: { + upsert: { + type: 'terminal', + requires: ['hz_permissions'], + handler: upsert(ctx), + }, + }, + }), +}); diff --git a/plugins/src/watch.js b/plugins/src/watch.js index 2a8d8a074..996b7785d 100644 --- a/plugins/src/watch.js +++ b/plugins/src/watch.js @@ -1,6 +1,7 @@ 'use strict'; -const common = require('./common'); +const utils = require('./common/utils'); +const common = require('./common/reads'); function watch(server) { return (req, res, next) => { @@ -21,7 +22,7 @@ function watch(server) { include_offsets: req.getParameter('order') !== undefined && req.getParameter('limit') !== undefined, - }).run(conn, common.reql_options) + }).run(conn, utils.reqlOptions) ).then((feed) => { res.complete.then(() => { feed.close().catch(() => { }); diff --git a/plugins/src/writes/index.js b/plugins/src/writes/index.js deleted file mode 100644 index 155385872..000000000 --- a/plugins/src/writes/index.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const insert = require('./insert'); -const store = require('./store'); -const replace = require('./replace'); -const upsert = require('./upsert'); -const update = require('./update'); -const remove = require('./remove'); - -module.exports = (raw_config) => ({ - name: (raw_config && raw_config.name) || 'hz_writes', - activate: (server) => ({ - methods: { - insert: { - requires: ['hz_permissions'], - type: 'terminal', - handler: insert(server), - }, - store: { - requires: ['hz_permissions'], - type: 'terminal', - handler: store(server), - }, - replace: { - requires: ['hz_permissions'], - type: 'terminal', - handler: replace(server), - }, - upsert: { - requires: ['hz_permissions'], - type: 'terminal', - handler: upsert(server), - }, - update: { - requires: ['hz_permissions'], - type: 'terminal', - handler: update(server), - }, - remove: { - requires: ['hz_permissions'], - type: 'terminal', - handler: remove(server), - }, - }, - }), -}); From 027d064e441019b7f777c371b1f4341117b172ac Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 03:51:41 -0700 Subject: [PATCH 50/86] fixing eslint errors in plugins --- plugins/src/above.js | 4 ++-- plugins/src/below.js | 4 ++-- plugins/src/collection.js | 33 ++++++++++++++++++---------- plugins/src/collection/collection.js | 4 ++-- plugins/src/collection/index.js | 10 +++++++-- plugins/src/collection/metadata.js | 20 +++++++++++------ plugins/src/common/reads.js | 4 ++-- plugins/src/common/utils.js | 2 +- plugins/src/fetch.js | 4 ++-- plugins/src/find.js | 2 +- plugins/src/findAll.js | 2 +- plugins/src/limit.js | 2 +- plugins/src/order.js | 2 +- plugins/src/permissions.js | 3 +-- plugins/src/permit_all.js | 2 +- plugins/src/store.js | 6 ++--- plugins/src/timeout.js | 2 +- plugins/src/upsert.js | 4 ++-- plugins/src/watch.js | 6 ++--- 19 files changed, 69 insertions(+), 47 deletions(-) diff --git a/plugins/src/above.js b/plugins/src/above.js index 07bc63d93..1a8922fb0 100644 --- a/plugins/src/above.js +++ b/plugins/src/above.js @@ -16,9 +16,9 @@ function above(req, res, next) { } } -module.exports = (options) => ({ +module.exports = () => ({ name: 'hz_above', - activate: (ctx) => ({ + activate: () => ({ methods: { above: { type: 'option', diff --git a/plugins/src/below.js b/plugins/src/below.js index a9a7a9d46..fcb17e2ba 100644 --- a/plugins/src/below.js +++ b/plugins/src/below.js @@ -16,9 +16,9 @@ function below(req, res, next) { } } -module.exports = (options) => ({ +module.exports = () => ({ name: 'hz_below', - activate: (ctx) => ({ + activate: () => ({ methods: { below: { type: 'option', diff --git a/plugins/src/collection.js b/plugins/src/collection.js index 64c33c4dc..0b7dc57b1 100644 --- a/plugins/src/collection.js +++ b/plugins/src/collection.js @@ -2,7 +2,7 @@ const {ReliableMetadata} = require('./common/metadata.js'); -function collection(server, metadata) { +function collection(metadata) { return (req, res, next) => { const args = req.options.collection; if (args.length !== 1) { @@ -10,36 +10,47 @@ function collection(server, metadata) { } else if (typeof args[0] !== 'string') { next(new Error('First argument to "collection" must be a string.')); } else { - metadata.collection(args[0]).then((collection) => { + metadata.collection(args[0]).then((c) => { // RSI: pick up here trucks here reads aren't getting this? - req.setParameter(collection); + req.setParameter(c); next(); }).catch(next); } }; } -module.exports = (options) => ({ +module.exports = (options) => { + const metadataSymbol = Symbol(); + return { name: 'hz_collection', - activate: (server, onReady, onUnready) => { + activate: (ctx, onReady, onUnready) => { const metadata = new ReliableMetadata( - server, - raw_config.auto_create_collection, - raw_config.auto_create_index); + ctx, + options.auto_create_collection, + options.auto_create_index); + + ctx[metadataSymbol] = metadata; + metadata.subscribe({onReady: () => { console.log('metadata ready'); onReady(); }, onUnready}); + return { methods: { collection: { type: 'option', - handler: collection(server, metadata), + handler: collection(metadata), }, }, - deactivate: () => metadata.close(), }; }, + deactivate: (ctx) => { + const metadata = ctx[metadataSymbol]; + if (metadata) { + metadata.close(); + } + }, }; -}); +}; diff --git a/plugins/src/collection/collection.js b/plugins/src/collection/collection.js index 8b2d98ef0..03ec02827 100644 --- a/plugins/src/collection/collection.js +++ b/plugins/src/collection/collection.js @@ -1,6 +1,6 @@ 'use strict'; -const {r, logger} = require('@horizon/server'); +const {r} = require('@horizon/server'); const Table = require('./table').Table; @@ -10,7 +10,7 @@ class Collection { this.reliableConn = reliableConn; this.table = r.db(db).table(name); // This is the ReQL Table object this._tables = new Map(); // A Map of Horizon Table objects - this._registered = false; // Whether the `hz_collections` table says this collection exists + this._registered = false; // Whether the `hz_collections` table thinks this exists this._waiters = []; } diff --git a/plugins/src/collection/index.js b/plugins/src/collection/index.js index f2a052190..4a4024420 100644 --- a/plugins/src/collection/index.js +++ b/plugins/src/collection/index.js @@ -27,7 +27,11 @@ const name_to_info = (name) => { const json_offset = matches[0].length - 1; - const info = {name, geo: Boolean(matches[1]), multi: isNaN(matches[2]) ? false : Number(matches[2])}; + const info = { + name, + geo: Boolean(matches[1]), + multi: isNaN(matches[2]) ? false : Number(matches[2]), + }; // Parse remainder as JSON try { @@ -183,7 +187,9 @@ class Index { for (let i = 0; i < ordered_fields.length; ++i) { const pos = this.fields.length - ordered_fields.length + i; - if (pos < 0 || !compare_fields(ordered_fields[i], this.fields[pos])) { return false; } + if (pos < 0 || !compare_fields(ordered_fields[i], this.fields[pos])) { + return false; + } } return true; diff --git a/plugins/src/collection/metadata.js b/plugins/src/collection/metadata.js index b6c875298..55837fa49 100644 --- a/plugins/src/collection/metadata.js +++ b/plugins/src/collection/metadata.js @@ -5,11 +5,11 @@ const utils = require('./utils'); const assert = require('assert'); -import {r, logger, Reliable, ReliableUnion} from '@horizon/server'; +const {r, logger, Reliable, ReliableUnion} = require('@horizon/server'); const metadata_version = [2, 0, 0]; -export const create_collection = (db, name, conn) => +const createCollection = (db, name, conn) => r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => r.branch( res('errors').ne(0), @@ -20,7 +20,7 @@ export const create_collection = (db, name, conn) => ) ).run(conn); -export const initialize_metadata = (db, conn) => +const initializeMetadata = (db, conn) => r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) .then(() => Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => @@ -33,7 +33,7 @@ export const initialize_metadata = (db, conn) => .then(() => Promise.all([ r.db(db).tableList().contains('users').not().run(conn).then(() => - create_collection(r, db, 'users', conn)), + createCollection(r, db, 'users', conn)), r.db(db).table('hz_collections') .insert({id: 'hz_metadata', version: metadata_version}) .run(conn), @@ -89,7 +89,7 @@ class ReliableInit extends Reliable { this.check_attempt(attempt); logger.debug('checking for internal tables'); if (this._auto_create_collection) { - return initialize_metadata(this._db, conn); + return initializeMetadata(this._db, conn); } else { return r.dbList().contains(this._db).run(conn).then((has_db) => { if (!has_db) { @@ -154,7 +154,7 @@ class ReliableInit extends Reliable { } } -export class ReliableMetadata extends Reliable { +class ReliableMetadata extends Reliable { constructor(server, auto_create_collection, auto_create_index) { @@ -346,7 +346,7 @@ export class ReliableMetadata extends Reliable { collection = new Collection(this._db, name, this._reliable_conn); this._collections.set(name, collection); - return create_collection(this._db, name, this._reliable_conn.connection()); + return createCollection(this._db, name, this._reliable_conn.connection()); }).then((res) => { assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); logger.warn(`Collection created: "${name}"`); @@ -367,3 +367,9 @@ export class ReliableMetadata extends Reliable { }); } } + +module.exports = { + createCollection, + initializeMetadata, + ReliableMetadata, +}; diff --git a/plugins/src/common/reads.js b/plugins/src/common/reads.js index 7550da70c..3aedb4896 100644 --- a/plugins/src/common/reads.js +++ b/plugins/src/common/reads.js @@ -14,7 +14,7 @@ function object_to_fields(obj) { } // This is exposed to be reused by 'subscribe' -const make_reql = (req) => Promise.resolve().then(() => { +const makeReadReql = (req) => Promise.resolve().then(() => { const find = req.getParameter('find'); const limit = req.getParameter('limit'); const order = req.getParameter('order'); @@ -120,4 +120,4 @@ const make_reql = (req) => Promise.resolve().then(() => { return reqlPromise.then((reql) => (limit !== undefined ? reql.limit(limit) : reql)); }); -module.exports = {make_reql, reql_options}; +module.exports = {makeReadReql}; diff --git a/plugins/src/common/utils.js b/plugins/src/common/utils.js index 211009628..15d74d752 100644 --- a/plugins/src/common/utils.js +++ b/plugins/src/common/utils.js @@ -24,7 +24,7 @@ function rethinkdbVersionCheck(version_string) { // Convert strings to ints and remove first match const versions = matches.slice(1).map((val) => parseInt(val)); - if (!version_compare(versions, MIN_VERSION)) { + if (!versionCompare(versions, MIN_VERSION)) { throw new Error(`RethinkDB (${versions.join('.')}) is below required version ` + `(${MIN_VERSION.join('.')}) for use with Horizon.`); } diff --git a/plugins/src/fetch.js b/plugins/src/fetch.js index 336580ba5..bc552e47c 100644 --- a/plugins/src/fetch.js +++ b/plugins/src/fetch.js @@ -1,7 +1,7 @@ 'use strict'; const utils = require('./common/utils'); -const common = require('./common/reads'); +const {makeReadReql} = require('./common/reads'); function fetch(ctx) { return (req, res, next) => { @@ -14,7 +14,7 @@ function fetch(ctx) { } else if (!permissions) { next(new Error('"fetch" requires permissions to run')); } else { - common.make_reql(req).then((reql) => + makeReadReql(req).then((reql) => reql.run(conn, utils.reqlOptions) ).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { diff --git a/plugins/src/find.js b/plugins/src/find.js index 51bb0ee76..d55d7d8e4 100644 --- a/plugins/src/find.js +++ b/plugins/src/find.js @@ -16,7 +16,7 @@ function find(req, res, next) { module.exports = () => ({ name: 'hz_find', - activate: (ctx) => ({ + activate: () => ({ methods: { find: { type: 'option', diff --git a/plugins/src/findAll.js b/plugins/src/findAll.js index 680006bc9..ff2fa20d2 100644 --- a/plugins/src/findAll.js +++ b/plugins/src/findAll.js @@ -16,7 +16,7 @@ function findAll(req, res, next) { module.exports = () => ({ name: 'hz_findAll', - activate: (ctx) => ({ + activate: () => ({ methods: { findAll: { type: 'option', diff --git a/plugins/src/limit.js b/plugins/src/limit.js index 5f7c5bd61..f13dc0856 100644 --- a/plugins/src/limit.js +++ b/plugins/src/limit.js @@ -14,7 +14,7 @@ function limit(req, res, next) { module.exports = () => ({ name: 'hz_limit', - activate: (ctx) => ({ + activate: () => ({ methods: { limit: { type: 'option', diff --git a/plugins/src/order.js b/plugins/src/order.js index 4c52bf38a..e42cfd98d 100644 --- a/plugins/src/order.js +++ b/plugins/src/order.js @@ -32,7 +32,7 @@ function order(req, res, next) { module.exports = () => ({ name: 'hz_order', - activate: (ctx) => ({ + activate: () => ({ methods: { order: { type: 'option', diff --git a/plugins/src/permissions.js b/plugins/src/permissions.js index 5ad44e247..ddfd281ff 100644 --- a/plugins/src/permissions.js +++ b/plugins/src/permissions.js @@ -308,8 +308,7 @@ module.exports = (raw_config) => { const name = config.name; const userCache = Symbol(`${name}_userCache`); const userSub = Symbol(`${name}_userSub`); - let authCb; - let disconnectCb; + let authCb, disconnectCb; return { name: 'hz_permissions', diff --git a/plugins/src/permit_all.js b/plugins/src/permit_all.js index 51c73d255..153cebd79 100644 --- a/plugins/src/permit_all.js +++ b/plugins/src/permit_all.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = (raw_config) => ({ +module.exports = () => ({ name: 'hz_permissions', activate: () => ({ methods: { diff --git a/plugins/src/store.js b/plugins/src/store.js index cf5916bb2..fecc90e03 100644 --- a/plugins/src/store.js +++ b/plugins/src/store.js @@ -6,9 +6,9 @@ const hz_v = utils.versionField; const {r} = require('@horizon/server'); -function store(server) { +function store(ctx) { return (request, response, next) => { - const conn = server.rdb_connection().connection(); + const conn = ctx.rdb_connection().connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -53,7 +53,7 @@ function store(server) { ) ), {returnChanges: 'always'}), - // The new row does not have an id, so we insert it with an autogen id + // The new row does not have an id, so it will autogenerate collection.table.insert(common.apply_version(new_row, 0), {returnChanges: 'always'}))) .run(conn, utils.reqlOptions) diff --git a/plugins/src/timeout.js b/plugins/src/timeout.js index 54b8ef46b..686a42d41 100644 --- a/plugins/src/timeout.js +++ b/plugins/src/timeout.js @@ -12,7 +12,7 @@ function timeout(req, res, next) { } } -module.exports = (raw_config) => ({ +module.exports = () => ({ name: 'hz_timeout', activate: () => ({ methods: ({ diff --git a/plugins/src/upsert.js b/plugins/src/upsert.js index 74e982aee..9a4373b94 100644 --- a/plugins/src/upsert.js +++ b/plugins/src/upsert.js @@ -54,13 +54,13 @@ function upsert(server) { old_row(hz_v).default(-1).ne(new_row(hz_v))), r.error(common.invalidated_msg), - // Otherwise, we can update the row and increment the version + // Otherwise, we can update the row and version common.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), - // The new row did not have an id, so we insert it with an autogen id + // The new row did not have an id, so it will autogenerate collection.table.insert(common.apply_version(new_row, 0), {returnChanges: 'always'}))) .run(conn, utils.reqlOptions) diff --git a/plugins/src/watch.js b/plugins/src/watch.js index 996b7785d..96d357925 100644 --- a/plugins/src/watch.js +++ b/plugins/src/watch.js @@ -1,7 +1,7 @@ 'use strict'; const utils = require('./common/utils'); -const common = require('./common/reads'); +const {makeReadReql} = require('./common/reads'); function watch(server) { return (req, res, next) => { @@ -14,8 +14,8 @@ function watch(server) { } else if (!permissions) { next(new Error('"watch" requires permissions to run')); } else { - common.make_reql(req).then((reql) => - reql.changes({ + makeReadReql(req).then((reql) => + reql.changes({ include_initial: true, include_states: true, include_types: true, From 26613f451a5a1fbceefdc3a8e9ac8353afadc022 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 04:25:22 -0700 Subject: [PATCH 51/86] working on getting the test server running again --- babelify.sh | 2 +- cli/src/create-cert.js | 6 +- cli/src/init.js | 6 +- cli/src/make-token.js | 30 ++- cli/src/migrate.js | 81 ++++---- cli/src/schema.js | 160 ++++++++-------- cli/src/serve.js | 181 +++++++++--------- cli/src/utils/config.js | 16 +- cli/src/utils/initialize_joi.js | 2 +- cli/src/utils/interrupt.js | 4 +- cli/src/utils/start_rdb_server.js | 12 +- {plugin_router => plugin-router}/.babelrc | 0 {plugin_router => plugin-router}/.eslintrc.js | 0 {plugin_router => plugin-router}/package.json | 5 +- {plugin_router => plugin-router}/src/index.js | 0 .../src/request.js | 0 plugins/src/collection.js | 2 +- plugins/src/collection/metadata.js | 2 +- plugins/src/fetch.js | 4 +- plugins/src/insert.js | 12 +- plugins/src/permissions.js | 2 +- plugins/src/permissions/template.js | 4 +- plugins/src/permissions/validator.js | 4 +- plugins/src/remove.js | 15 +- plugins/src/replace.js | 19 +- plugins/src/store.js | 23 ++- plugins/src/update.js | 19 +- plugins/src/upsert.js | 23 ++- plugins/src/watch.js | 4 +- test/package.json | 38 ++++ test/serve.js | 54 +++--- 31 files changed, 382 insertions(+), 348 deletions(-) rename {plugin_router => plugin-router}/.babelrc (100%) rename {plugin_router => plugin-router}/.eslintrc.js (100%) rename {plugin_router => plugin-router}/package.json (88%) rename {plugin_router => plugin-router}/src/index.js (100%) rename {plugin_router => plugin-router}/src/request.js (100%) create mode 100644 test/package.json diff --git a/babelify.sh b/babelify.sh index 30bee422f..3fdaf6a1f 100755 --- a/babelify.sh +++ b/babelify.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -dirs="server server-utils plugins plugin_router" +dirs="server plugins plugin-router" for path in `find $dirs -name .babelrc | grep -v node_modules`; do { babel ${path%%.babelrc}/src -d ${path%%.babelrc}/dist -s true -w diff --git a/cli/src/create-cert.js b/cli/src/create-cert.js index 740ea8cda..41f6894d9 100644 --- a/cli/src/create-cert.js +++ b/cli/src/create-cert.js @@ -18,7 +18,7 @@ const run = (args) => { }; // generate the arguments to the command - const binArgs = [ 'req', '-x509', '-nodes', '-batch', + const binArgs = ['req', '-x509', '-nodes', '-batch', '-newkey', `${settings.algo}:${settings.bits}`, '-keyout', settings.keyOutName, '-out', settings.certOutName, @@ -39,8 +39,8 @@ const run = (args) => { const sslProc = spawn(settings.binaryName, binArgs); // pipe output appropriately - sslProc.stdout.pipe(process.stdout, { end: false }); - sslProc.stderr.pipe(process.stderr, { end: false }); + sslProc.stdout.pipe(process.stdout, {end: false}); + sslProc.stderr.pipe(process.stderr, {end: false}); // say nice things to the user when it's done sslProc.on('error', reject); diff --git a/cli/src/init.js b/cli/src/init.js index 258ee93cd..c04ce1cb6 100644 --- a/cli/src/init.js +++ b/cli/src/init.js @@ -166,9 +166,9 @@ rethinkdb_data `; const parseArguments = (args) => { - const parser = new argparse.ArgumentParser({ prog: 'hz init' }); - parser.addArgument([ 'projectName' ], - { action: 'store', + const parser = new argparse.ArgumentParser({prog: 'hz init'}); + parser.addArgument(['projectName'], + {action: 'store', help: 'Name of directory to create. Defaults to current directory', nargs: '?', } diff --git a/cli/src/make-token.js b/cli/src/make-token.js index af7202b16..b1419abc7 100644 --- a/cli/src/make-token.js +++ b/cli/src/make-token.js @@ -1,33 +1,29 @@ 'use strict'; -const interrupt = require('./utils/interrupt'); const config = require('./utils/config'); -const horizon_server = require('@horizon/server'); const path = require('path'); const jwt = require('jsonwebtoken'); -const r = horizon_server.r; -const logger = horizon_server.logger; const argparse = require('argparse'); const parseArguments = (args) => { - const parser = new argparse.ArgumentParser({ prog: 'hz make-token' }); + const parser = new argparse.ArgumentParser({prog: 'hz make-token'}); parser.addArgument( - [ '--config' ], - { type: 'string', metavar: 'PATH', - help: 'Path to the config file to use, defaults to ".hz/config.toml".' }); + ['--config'], + {type: 'string', metavar: 'PATH', + help: 'Path to the config file to use, defaults to ".hz/config.toml".'}); parser.addArgument( - [ '--token-secret' ], - { type: 'string', metavar: 'SECRET', - help: 'Secret key for signing the token.' }); + ['--token-secret'], + {type: 'string', metavar: 'SECRET', + help: 'Secret key for signing the token.'}); parser.addArgument( - [ 'user' ], - { type: 'string', metavar: 'USER_ID', - help: 'The ID of the user to issue a token for.' }); + ['user'], + {type: 'string', metavar: 'USER_ID', + help: 'The ID of the user to issue a token for.'}); return parser.parseArgs(args); }; @@ -48,7 +44,7 @@ const processConfig = (parsed) => { options.project_name = path.basename(path.resolve(options.project_path)); } - return Object.assign(options, { user: parsed.user }); + return Object.assign(options, {user: parsed.user}); }; const run = (args) => Promise.resolve().then(() => { @@ -58,9 +54,9 @@ const run = (args) => Promise.resolve().then(() => { throw new Error('No token secret specified, unable to sign the token.'); } const token = jwt.sign( - { id: options.user, provider: null }, + {id: options.user, provider: null}, new Buffer(options.token_secret, 'base64'), - { expiresIn: '1d', algorithm: 'HS512' } + {expiresIn: '1d', algorithm: 'HS512'} ); console.log(`${token}`); }); diff --git a/cli/src/migrate.js b/cli/src/migrate.js index dbd0980c7..5daf3bf0e 100644 --- a/cli/src/migrate.js +++ b/cli/src/migrate.js @@ -13,12 +13,12 @@ const change_to_project_dir = require('./utils/change_to_project_dir'); const parse_yes_no_option = require('./utils/parse_yes_no_option'); const start_rdb_server = require('./utils/start_rdb_server'); -const VERSION_2_0 = [ 2, 0, 0 ]; +const VERSION_2_0 = [2, 0, 0]; function run(cmdArgs) { const options = processConfig(cmdArgs); interrupt.on_interrupt(() => teardown()); - return Promise.resolve().bind({ options }) + return Promise.resolve().bind({options}) .then(setup) .then(validateMigration) .then(makeBackup) @@ -44,50 +44,50 @@ function white() { function processConfig(cmdArgs) { // do boilerplate to get config args :/ - const parser = new argparse.ArgumentParser({ prog: 'hz migrate' }); + const parser = new argparse.ArgumentParser({prog: 'hz migrate'}); - parser.addArgument([ 'project_path' ], { + parser.addArgument(['project_path'], { default: '.', nargs: '?', help: 'Change to this directory before migrating', }); - parser.addArgument([ '--project-name', '-n' ], { + parser.addArgument(['--project-name', '-n'], { help: 'Name of the Horizon project server', }); - parser.addArgument([ '--connect', '-c' ], { + parser.addArgument(['--connect', '-c'], { metavar: 'host:port', default: undefined, help: 'Host and port of the RethinkDB server to connect to.', }); - parser.addArgument([ '--rdb-user' ], { + parser.addArgument(['--rdb-user'], { default: 'admin', metavar: 'USER', help: 'RethinkDB User', }); - parser.addArgument([ '--rdb-password' ], { + parser.addArgument(['--rdb-password'], { default: undefined, metavar: 'PASSWORD', help: 'RethinkDB Password', }); - parser.addArgument([ '--start-rethinkdb' ], { + parser.addArgument(['--start-rethinkdb'], { metavar: 'yes|no', default: 'yes', constant: 'yes', nargs: '?', - help: 'Start up a RethinkDB server in the current directory' + help: 'Start up a RethinkDB server in the current directory', }); - parser.addArgument([ '--config' ], { + parser.addArgument(['--config'], { default: '.hz/config.toml', help: 'Path to the config file to use, defaults to ".hz/config.toml".', }); - parser.addArgument([ '--skip-backup' ], { + parser.addArgument(['--skip-backup'], { metavar: 'yes|no', default: 'no', constant: 'yes', @@ -134,7 +134,7 @@ function setup() { // start rethinkdb server if necessary if (this.options.start_rethinkdb) { green(' ├── Starting RethinkDB server'); - return start_rdb_server({ quiet: true }).then((server) => { + return start_rdb_server({quiet: true}).then((server) => { this.rdb_server = server; this.options.rdb_host = 'localhost'; this.options.rdb_port = server.driver_port; @@ -177,16 +177,16 @@ function validateMigration() { const checkForHzTables = r.db('rethinkdb') .table('table_config') - .filter({ db: project })('name') + .filter({db: project})('name') .contains((x) => x.match('^hz_')) .branch(r.error( `Some tables in ${project} have an hz_ prefix`), true); const waitForCollections = r.db(`${project}_internal`) .table('collections') - .wait({ timeout: 30 }) + .wait({timeout: 30}) .do(() => r.db(project).tableList()) .forEach((tableName) => - r.db(project).table(tableName).wait({ timeout: 30 }) + r.db(project).table(tableName).wait({timeout: 30}) ); return Promise.resolve().then(() => { @@ -241,11 +241,11 @@ function renameUserTables() { const project = this.options.project_name; return Promise.resolve().then(() => { white('Removing suffix from user tables'); - return r.db(`${project}_internal`).wait({ timeout: 30 }). + return r.db(`${project}_internal`).wait({timeout: 30}). do(() => r.db(`${project}_internal`).table('collections') .forEach((collDoc) => r.db('rethinkdb').table('table_config') - .filter({ db: project, name: collDoc('table') }) - .update({ name: collDoc('id') })) + .filter({db: project, name: collDoc('table')}) + .update({name: collDoc('id')})) ).run(this.conn) .then(() => green(' └── Suffixes removed')); }); @@ -259,7 +259,7 @@ function moveInternalTables() { return Promise.resolve().then(() => { white(`Moving internal tables from ${project}_internal to ${project}`); return r.db('rethinkdb').table('table_config') - .filter({ db: `${project}_internal` }) + .filter({db: `${project}_internal`}) .update((table) => ({ db: project, name: r.branch( @@ -283,11 +283,11 @@ function renameIndices() { const project = this.options.project_name; return Promise.resolve().then(() => { white('Renaming indices to new JSON format'); - return r.db(project).tableList().forEach((tableName) => { - return r.db(project).table(tableName).indexList().forEach((indexName) => { - return r.db(project).table(tableName).indexRename(indexName, rename(indexName)); - }); - }).run(this.conn) + return r.db(project).tableList().forEach((tableName) => + r.db(project).table(tableName).indexList().forEach((indexName) => + r.db(project).table(tableName).indexRename(indexName, rename(indexName)) + ) + ).run(this.conn) .then(() => green(' └── Indices renamed.')); }); @@ -296,34 +296,31 @@ function renameIndices() { const initialState = { escaped: false, field: '', - fields: [ ], + fields: [], }; return name.split('') - .fold(initialState, (acc, c) => { - return r.branch( + .fold(initialState, (acc, c) => + r.branch( acc('escaped'), acc.merge({ escaped: false, field: acc('field').add(c), }), c.eq('\\'), - acc.merge({ escaped: true }), + acc.merge({escaped: true}), c.eq('_'), acc.merge({ fields: acc('fields').append(acc('field')), field: '', }), - acc.merge({ field: acc('field').add(c) }) - ); - }) - .do((state) => - // last field needs to be appended to running list - state('fields').append(state('field')) + acc.merge({field: acc('field').add(c)}) + ) + ).do((state) => + // last field needs to be appended to running list + state('fields').append(state('field')) // wrap each field in an array - .map((field) => [ field ]) - ) - .toJSON() - .do((x) => r('hz_').add(x)); + .map((field) => [field]) + ).toJSON().do((x) => r('hz_').add(x)); } } @@ -334,15 +331,15 @@ function rewriteHzCollectionDocs() { return Promise.resolve().then(() => { white('Rewriting hz_collections to new format'); return r.db(project).table('hz_collections') - .update({ table: r.literal() }) + .update({table: r.literal()}) .run(this.conn); }).then(() => green(' ├── "table" field removed')) .then(() => r.db(project).table('hz_collections') - .insert({ id: 'users' }) + .insert({id: 'users'}) .run(this.conn)) .then(() => green(' ├── Added document for "users" table')) .then(() => r.db(project).table('hz_collections') - .insert({ id: 'hz_metadata', version: VERSION_2_0 }) + .insert({id: 'hz_metadata', version: VERSION_2_0}) .run(this.conn)) .then(() => green(' └── Adding the metadata document with schema version:' + `${JSON.stringify(VERSION_2_0)}`)); diff --git a/cli/src/schema.js b/cli/src/schema.js index 974917459..84f2f53c4 100644 --- a/cli/src/schema.js +++ b/cli/src/schema.js @@ -25,72 +25,72 @@ const initialize_metadata = horizon_metadata.initialize_metadata; initialize_joi(Joi); const parseArguments = (args) => { - const parser = new argparse.ArgumentParser({ prog: 'hz schema' }); + const parser = new argparse.ArgumentParser({prog: 'hz schema'}); const subparsers = parser.addSubparsers({ title: 'subcommands', dest: 'subcommand_name', }); - const apply = subparsers.addParser('apply', { addHelp: true }); - const save = subparsers.addParser('save', { addHelp: true }); + const apply = subparsers.addParser('apply', {addHelp: true}); + const save = subparsers.addParser('save', {addHelp: true}); // Set options shared between both subcommands - [ apply, save ].map((subcmd) => { - subcmd.addArgument([ 'project_path' ], - { type: 'string', nargs: '?', - help: 'Change to this directory before serving' }); - - subcmd.addArgument([ '--project-name', '-n' ], - { type: 'string', action: 'store', metavar: 'NAME', - help: 'Name of the Horizon Project server' }); - - subcmd.addArgument([ '--connect', '-c' ], - { type: 'string', metavar: 'HOST:PORT', - help: 'Host and port of the RethinkDB server to connect to.' }); - - subcmd.addArgument([ '--rdb-timeout' ], - { type: 'int', metavar: 'TIMEOUT', - help: 'Timeout period in seconds for the RethinkDB connection to be opened' }); - - subcmd.addArgument([ '--rdb-user' ], - { type: 'string', metavar: 'USER', - help: 'RethinkDB User' }); - - subcmd.addArgument([ '--rdb-password' ], - { type: 'string', metavar: 'PASSWORD', - help: 'RethinkDB Password' }); - - subcmd.addArgument([ '--start-rethinkdb' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Start up a RethinkDB server in the current directory' }); - - subcmd.addArgument([ '--config' ], - { type: 'string', metavar: 'PATH', - help: 'Path to the config file to use, defaults to ".hz/config.toml".' }); - - subcmd.addArgument([ '--debug' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Enable debug logging.' }); + [apply, save].map((subcmd) => { + subcmd.addArgument(['project_path'], + {type: 'string', nargs: '?', + help: 'Change to this directory before serving'}); + + subcmd.addArgument(['--project-name', '-n'], + {type: 'string', action: 'store', metavar: 'NAME', + help: 'Name of the Horizon Project server'}); + + subcmd.addArgument(['--connect', '-c'], + {type: 'string', metavar: 'HOST:PORT', + help: 'Host and port of the RethinkDB server to connect to.'}); + + subcmd.addArgument(['--rdb-timeout'], + {type: 'int', metavar: 'TIMEOUT', + help: 'Timeout period in seconds for the RethinkDB connection to be opened'}); + + subcmd.addArgument(['--rdb-user'], + {type: 'string', metavar: 'USER', + help: 'RethinkDB User'}); + + subcmd.addArgument(['--rdb-password'], + {type: 'string', metavar: 'PASSWORD', + help: 'RethinkDB Password'}); + + subcmd.addArgument(['--start-rethinkdb'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Start up a RethinkDB server in the current directory'}); + + subcmd.addArgument(['--config'], + {type: 'string', metavar: 'PATH', + help: 'Path to the config file to use, defaults to ".hz/config.toml".'}); + + subcmd.addArgument(['--debug'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Enable debug logging.'}); }); // Options exclusive to HZ SCHEMA APPLY - apply.addArgument([ '--update' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Only add new items and update existing, no removal.' }); + apply.addArgument(['--update'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Only add new items and update existing, no removal.'}); - apply.addArgument([ '--force' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Allow removal of existing collections.' }); + apply.addArgument(['--force'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Allow removal of existing collections.'}); - apply.addArgument([ 'schema_file' ], - { type: 'string', metavar: 'SCHEMA_FILE_PATH', - help: 'File to get the horizon schema from, use "-" for stdin.' }); + apply.addArgument(['schema_file'], + {type: 'string', metavar: 'SCHEMA_FILE_PATH', + help: 'File to get the horizon schema from, use "-" for stdin.'}); // Options exclusive to HZ SCHEMA SAVE - save.addArgument([ '--out-file', '-o' ], - { type: 'string', metavar: 'PATH', defaultValue: '.hz/schema.toml', - help: 'File to write the horizon schema to, defaults to .hz/schema.toml.' }); + save.addArgument(['--out-file', '-o'], + {type: 'string', metavar: 'PATH', defaultValue: '.hz/schema.toml', + help: 'File to write the horizon schema to, defaults to .hz/schema.toml.'}); return parser.parseArgs(args); }; @@ -105,7 +105,7 @@ const schema_schema = Joi.object().unknown(false).keys({ fields: Joi.array().items(Joi.array().items(Joi.string())).required(), }) ) - ).optional().default([ ]), + ).optional().default([]), }) ).optional().default({ }), groups: Joi.object().unknown(true).pattern(/.*/, @@ -124,7 +124,7 @@ const schema_schema = Joi.object().unknown(false).keys({ const v1_0_name_to_fields = (name) => { let escaped = false; let field = ''; - const fields = [ ]; + const fields = []; for (const c of name) { if (escaped) { if (c !== '\\' && c !== '_') { @@ -144,7 +144,7 @@ const v1_0_name_to_fields = (name) => { if (escaped) { throw new Error(`Unexpected index name: "${name}"`); } - fields.push([ field ]); + fields.push([field]); return fields; }; @@ -156,15 +156,15 @@ const parse_schema = (schema_toml) => { throw parsed.error; } - const collections = [ ]; + const collections = []; for (const name in schema.collections) { collections.push({ id: name, indexes: schema.collections[name].indexes.map((index) => { if (typeof index === 'string') { - return { fields: v1_0_name_to_fields(index), multi: false, geo: false }; + return {fields: v1_0_name_to_fields(index), multi: false, geo: false}; } else { - return { fields: index.fields, multi: false, geo: false }; + return {fields: index.fields, multi: false, geo: false}; } }), }); @@ -173,15 +173,15 @@ const parse_schema = (schema_toml) => { // Make sure the 'users' collection is present, as some things depend on // its existence. if (!schema.collections || !schema.collections.users) { - collections.push({ id: 'users', indexes: [ ] }); + collections.push({id: 'users', indexes: []}); } - const groups = [ ]; + const groups = []; for (const name in schema.groups) { - groups.push(Object.assign({ id: name }, schema.groups[name])); + groups.push(Object.assign({id: name}, schema.groups[name])); } - return { groups, collections }; + return {groups, collections}; }; const processApplyConfig = (parsed) => { @@ -196,7 +196,7 @@ const processApplyConfig = (parsed) => { if (parsed.schema_file === '-') { in_file = process.stdin; } else { - in_file = fs.createReadStream(parsed.schema_file, { flags: 'r' }); + in_file = fs.createReadStream(parsed.schema_file, {flags: 'r'}); } if (options.project_name === null) { @@ -251,7 +251,7 @@ const processSaveConfig = (parsed) => { }; const schema_to_toml = (collections, groups) => { - const res = [ '# This is a TOML document' ]; + const res = ['# This is a TOML document']; for (const c of collections) { res.push(''); @@ -285,7 +285,7 @@ const schema_to_toml = (collections, groups) => { const runApplyCommand = (options) => { let conn, schema, rdb_server; - let obsolete_collections = [ ]; + let obsolete_collections = []; const db = options.project_name; const cleanup = () => @@ -311,18 +311,18 @@ const runApplyCommand = (options) => { schema = parse_schema(schema_toml); if (options.start_rethinkdb) { - return start_rdb_server({ quiet: !options.debug }).then((server) => { + return start_rdb_server({quiet: !options.debug}).then((server) => { rdb_server = server; options.rdb_host = 'localhost'; options.rdb_port = server.driver_port; }); } }).then(() => - r.connect({ host: options.rdb_host, + r.connect({host: options.rdb_host, port: options.rdb_port, user: options.rdb_user, password: options.rdb_password, - timeout: options.rdb_timeout }) + timeout: options.rdb_timeout}) ).then((rdb_conn) => { conn = rdb_conn; return initialize_metadata(db, conn); @@ -331,10 +331,10 @@ const runApplyCommand = (options) => { console.log('Initialized new application metadata.'); } // Wait for metadata tables to be writable - return r.expr([ 'hz_collections', 'hz_groups' ]) + return r.expr(['hz_collections', 'hz_groups']) .forEach((table) => r.db(db).table(table) - .wait({ waitFor: 'ready_for_writes', timeout: 30 })) + .wait({waitFor: 'ready_for_writes', timeout: 30})) .run(conn); }).then(() => { // Error if any collections will be removed @@ -392,7 +392,7 @@ const runApplyCommand = (options) => { } }), r.db(db).table('hz_groups') - .insert(schema.groups, { conflict: 'replace' }) + .insert(schema.groups, {conflict: 'replace'}) .run(conn).then((res) => { if (res.errors) { throw new Error(`Failed to write groups: ${res.first_error}`); @@ -402,7 +402,7 @@ const runApplyCommand = (options) => { } }).then(() => { // Ensure all collections exist and remove any obsolete collections - const promises = [ ]; + const promises = []; for (const c of schema.collections) { promises.push( create_collection(db, c.id, conn).then((res) => { @@ -417,7 +417,7 @@ const runApplyCommand = (options) => { r.db(db) .table('hz_collections') .get(c) - .delete({ returnChanges: 'always' })('changes')(0) + .delete({returnChanges: 'always'})('changes')(0) .do((res) => r.branch(res.hasFields('error'), res, @@ -433,7 +433,7 @@ const runApplyCommand = (options) => { return Promise.all(promises); }).then(() => { - const promises = [ ]; + const promises = []; // Ensure all indexes exist for (const c of schema.collections) { @@ -442,7 +442,7 @@ const runApplyCommand = (options) => { promises.push( r.branch(r.db(db).table(c.id).indexList().contains(name), { }, r.db(db).table(c.id).indexCreate(name, horizon_index.info_to_reql(info), - { geo: Boolean(info.geo), multi: (info.multi !== false) })) + {geo: Boolean(info.geo), multi: (info.multi !== false)})) .run(conn) .then((res) => { if (res.errors) { @@ -502,28 +502,28 @@ const runSaveCommand = (options) => { } }).then(() => { if (options.start_rethinkdb) { - return start_rdb_server({ quiet: !options.debug }).then((server) => { + return start_rdb_server({quiet: !options.debug}).then((server) => { rdb_server = server; options.rdb_host = 'localhost'; options.rdb_port = server.driver_port; }); } }).then(() => - r.connect({ host: options.rdb_host, + r.connect({host: options.rdb_host, port: options.rdb_port, user: options.rdb_user, password: options.rdb_password, - timeout: options.rdb_timeout }) + timeout: options.rdb_timeout}) ).then((rdb_conn) => { conn = rdb_conn; - return r.db(db).wait({ waitFor: 'ready_for_reads', timeout: 30 }).run(conn); + return r.db(db).wait({waitFor: 'ready_for_reads', timeout: 30}).run(conn); }).then(() => r.object('collections', r.db(db).table('hz_collections') .filter((row) => row('id').match('^hz_').not()) .coerceTo('array') .map((row) => - row.merge({ indexes: r.db(db).table(row('id')).indexList() })), + row.merge({indexes: r.db(db).table(row('id')).indexList()})), 'groups', r.db(db).table('hz_groups').coerceTo('array')) .run(conn) ).then((res) => @@ -538,7 +538,7 @@ const runSaveCommand = (options) => { } const output = (options.out_file === '-') ? process.stdout : - fs.createWriteStream(options.out_file, { flags: 'w', defaultEncoding: 'utf8' }); + fs.createWriteStream(options.out_file, {flags: 'w', defaultEncoding: 'utf8'}); // Output toml_str to schema.toml const toml_str = schema_to_toml(res.collections, res.groups); diff --git a/cli/src/serve.js b/cli/src/serve.js index 0209da14a..acd099faa 100644 --- a/cli/src/serve.js +++ b/cli/src/serve.js @@ -1,7 +1,6 @@ 'use strict'; const chalk = require('chalk'); -const crypto = require('crypto'); const fs = require('fs'); const get_type = require('mime-types').contentType; const http = require('http'); @@ -27,92 +26,92 @@ const default_rdb_port = 28015; const default_rdb_timeout = 20; const parseArguments = (args) => { - const parser = new argparse.ArgumentParser({ prog: 'hz serve' }); + const parser = new argparse.ArgumentParser({prog: 'hz serve'}); - parser.addArgument([ 'project_path' ], - { type: 'string', nargs: '?', - help: 'Change to this directory before serving' }); + parser.addArgument(['project_path'], + {type: 'string', nargs: '?', + help: 'Change to this directory before serving'}); - parser.addArgument([ '--project-name', '-n' ], - { type: 'string', action: 'store', metavar: 'NAME', + parser.addArgument(['--project-name', '-n'], + {type: 'string', action: 'store', metavar: 'NAME', help: 'Name of the Horizon project. Determines the name of ' + - 'the RethinkDB database that stores the project data.' }); + 'the RethinkDB database that stores the project data.'}); - parser.addArgument([ '--bind', '-b' ], - { type: 'string', action: 'append', metavar: 'HOST', - help: 'Local hostname to serve horizon on (repeatable).' }); + parser.addArgument(['--bind', '-b'], + {type: 'string', action: 'append', metavar: 'HOST', + help: 'Local hostname to serve horizon on (repeatable).'}); - parser.addArgument([ '--port', '-p' ], - { type: 'int', metavar: 'PORT', - help: 'Local port to serve horizon on.' }); + parser.addArgument(['--port', '-p'], + {type: 'int', metavar: 'PORT', + help: 'Local port to serve horizon on.'}); - parser.addArgument([ '--connect', '-c' ], - { type: 'string', metavar: 'HOST:PORT', - help: 'Host and port of the RethinkDB server to connect to.' }); + parser.addArgument(['--connect', '-c'], + {type: 'string', metavar: 'HOST:PORT', + help: 'Host and port of the RethinkDB server to connect to.'}); - parser.addArgument([ '--rdb-timeout' ], - { type: 'int', metavar: 'TIMEOUT', - help: 'Timeout period in seconds for the RethinkDB connection to be opened' }); + parser.addArgument(['--rdb-timeout'], + {type: 'int', metavar: 'TIMEOUT', + help: 'Timeout period in seconds for the RethinkDB connection to be opened'}); - parser.addArgument([ '--rdb-user' ], - { type: 'string', metavar: 'USER', - help: 'RethinkDB User' }); + parser.addArgument(['--rdb-user'], + {type: 'string', metavar: 'USER', + help: 'RethinkDB User'}); - parser.addArgument([ '--rdb-password' ], - { type: 'string', metavar: 'PASSWORD', - help: 'RethinkDB Password' }); + parser.addArgument(['--rdb-password'], + {type: 'string', metavar: 'PASSWORD', + help: 'RethinkDB Password'}); - parser.addArgument([ '--key-file' ], - { type: 'string', metavar: 'PATH', - help: 'Path to the key file to use, defaults to "./horizon-key.pem".' }); + parser.addArgument(['--key-file'], + {type: 'string', metavar: 'PATH', + help: 'Path to the key file to use, defaults to "./horizon-key.pem".'}); - parser.addArgument([ '--cert-file' ], - { type: 'string', metavar: 'PATH', - help: 'Path to the cert file to use, defaults to "./horizon-cert.pem".' }); + parser.addArgument(['--cert-file'], + {type: 'string', metavar: 'PATH', + help: 'Path to the cert file to use, defaults to "./horizon-cert.pem".'}); - parser.addArgument([ '--token-secret' ], - { type: 'string', metavar: 'SECRET', - help: 'Key for signing jwts' }); + parser.addArgument(['--token-secret'], + {type: 'string', metavar: 'SECRET', + help: 'Key for signing jwts'}); - parser.addArgument([ '--allow-unauthenticated' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Whether to allow unauthenticated Horizon connections.' }); + parser.addArgument(['--allow-unauthenticated'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Whether to allow unauthenticated Horizon connections.'}); - parser.addArgument([ '--allow-anonymous' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Whether to allow anonymous Horizon connections.' }); + parser.addArgument(['--allow-anonymous'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Whether to allow anonymous Horizon connections.'}); - parser.addArgument([ '--debug' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Enable debug logging.' }); + parser.addArgument(['--debug'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Enable debug logging.'}); - parser.addArgument([ '--secure' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + parser.addArgument(['--secure'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', help: 'Serve secure websockets, requires --key-file and ' + - '--cert-file if true, on by default.' }); + '--cert-file if true, on by default.'}); - parser.addArgument([ '--start-rethinkdb' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Start up a RethinkDB server in the current directory' }); + parser.addArgument(['--start-rethinkdb'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Start up a RethinkDB server in the current directory'}); - parser.addArgument([ '--auto-create-collection' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Create collections used by requests if they do not exist.' }); + parser.addArgument(['--auto-create-collection'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Create collections used by requests if they do not exist.'}); - parser.addArgument([ '--auto-create-index' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Create indexes used by requests if they do not exist.' }); + parser.addArgument(['--auto-create-index'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Create indexes used by requests if they do not exist.'}); - parser.addArgument([ '--permissions' ], - { type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', - help: 'Enables or disables checking permissions on requests.' }); + parser.addArgument(['--permissions'], + {type: 'string', metavar: 'yes|no', constant: 'yes', nargs: '?', + help: 'Enables or disables checking permissions on requests.'}); - parser.addArgument([ '--serve-static' ], - { type: 'string', metavar: 'PATH', nargs: '?', constant: './dist', - help: 'Serve static files from a directory, defaults to "./dist".' }); + parser.addArgument(['--serve-static'], + {type: 'string', metavar: 'PATH', nargs: '?', constant: './dist', + help: 'Serve static files from a directory, defaults to "./dist".'}); - parser.addArgument([ '--dev' ], - { action: 'storeTrue', + parser.addArgument(['--dev'], + {action: 'storeTrue', help: 'Runs the server in development mode, this sets ' + '--secure=no, ' + '--permissions=no, ' + @@ -121,33 +120,33 @@ const parseArguments = (args) => { '--start-rethinkdb=yes, ' + '--allow-unauthenticated=yes, ' + '--allow-anonymous=yes ' + - 'and --serve-static=./dist.' }); + 'and --serve-static=./dist.'}); - parser.addArgument([ '--config' ], - { type: 'string', metavar: 'PATH', - help: 'Path to the config file to use, defaults to "${config.default_config_file}".' }); + parser.addArgument(['--config'], + {type: 'string', metavar: 'PATH', + help: 'Path to the config file to use, defaults to "${config.default_config_file}".'}); - parser.addArgument([ '--schema-file' ], - { type: 'string', metavar: 'SCHEMA_FILE_PATH', + parser.addArgument(['--schema-file'], + {type: 'string', metavar: 'SCHEMA_FILE_PATH', help: 'Path to the schema file to use, ' + - 'will attempt to apply schema before starting Horizon server".' }); + 'will attempt to apply schema before starting Horizon server".'}); - parser.addArgument([ '--auth' ], - { type: 'string', action: 'append', metavar: 'PROVIDER,ID,SECRET', defaultValue: [ ], - help: 'Auth provider and options comma-separated, e.g. "facebook,,".' }); + parser.addArgument(['--auth'], + {type: 'string', action: 'append', metavar: 'PROVIDER,ID,SECRET', defaultValue: [], + help: 'Auth provider and options comma-separated, e.g. "facebook,,".'}); - parser.addArgument([ '--auth-redirect' ], - { type: 'string', metavar: 'URL', - help: 'The URL to redirect to upon completed authentication, defaults to "/".' }); + parser.addArgument(['--auth-redirect'], + {type: 'string', metavar: 'URL', + help: 'The URL to redirect to upon completed authentication, defaults to "/".'}); - parser.addArgument([ '--access-control-allow-origin' ], - { type: 'string', metavar: 'URL', - help: 'The URL of the host that can access auth settings, defaults to "".' }); + parser.addArgument(['--access-control-allow-origin'], + {type: 'string', metavar: 'URL', + help: 'The URL of the host that can access auth settings, defaults to "".'}); - parser.addArgument([ '--open' ], - { action: 'storeTrue', + parser.addArgument(['--open'], + {action: 'storeTrue', help: 'Open index.html in the static files folder once Horizon is ready to' + - ' receive connections' }); + ' receive connections'}); return parser.parseArgs(args); }; @@ -157,22 +156,22 @@ const parseArguments = (args) => { const serve_file = (filePath, res) => { fs.access(filePath, fs.R_OK | fs.F_OK, (exists) => { if (exists) { - res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.writeHead(404, {'Content-Type': 'text/plain'}); res.end(`File "${filePath}" not found\n`); } else { fs.lstat(filePath, (err, stats) => { if (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.writeHead(500, {'Content-Type': 'text/plain'}); res.end(`${err}\n`); } else if (stats.isFile()) { fs.readFile(filePath, 'binary', (err2, file) => { if (err2) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.writeHead(500, {'Content-Type': 'text/plain'}); res.end(`${err2}\n`); } else { const type = get_type(path.extname(filePath)) || false; if (type) { - res.writeHead(200, { 'Content-Type': type }); + res.writeHead(200, {'Content-Type': type}); } else { res.writeHead(200); } @@ -200,7 +199,7 @@ const file_server = (distDir) => (req, res) => { }; const initialize_servers = (ctor, opts) => { - const servers = [ ]; + const servers = []; let numReady = 0; return new Promise((resolve, reject) => { opts.bind.forEach((host) => { @@ -256,7 +255,7 @@ const read_cert_file = (file) => { const create_secure_servers = (opts) => { const key = read_cert_file(opts.key_file); const cert = read_cert_file(opts.cert_file); - return initialize_servers(() => new https.Server({ key, cert }), opts); + return initialize_servers(() => new https.Server({key, cert}), opts); }; // Command-line flags have the highest precedence, followed by environment variables, @@ -281,7 +280,7 @@ const processConfig = (parsed) => { } if (options.bind.indexOf('all') !== -1) { - options.bind = [ '0.0.0.0' ]; + options.bind = ['0.0.0.0']; } if (!options.rdb_host) { @@ -415,7 +414,7 @@ const run = (args, interruptor) => { throw new Error(`Unrecognized auth provider "${name}"`); } hz_server.add_auth_provider(provider, - Object.assign({}, { path: name }, opts.auth[name])); + Object.assign({}, {path: name}, opts.auth[name])); } } }).then(() => { diff --git a/cli/src/utils/config.js b/cli/src/utils/config.js index a66203d91..284ddc331 100644 --- a/cli/src/utils/config.js +++ b/cli/src/utils/config.js @@ -19,7 +19,7 @@ const make_default_options = () => ({ // Default to current directory name for project name project_name: null, - bind: [ 'localhost' ], + bind: ['localhost'], port: 8181, start_rethinkdb: false, @@ -52,14 +52,14 @@ const make_default_options = () => ({ const default_options = make_default_options(); -const yes_no_options = [ 'debug', +const yes_no_options = ['debug', 'secure', 'permissions', 'start_rethinkdb', 'auto_create_index', 'auto_create_collection', 'allow_unauthenticated', - 'allow_anonymous' ]; + 'allow_anonymous']; const parse_connect = (connect, config) => { // support rethinkdb:// style connection uri strings @@ -104,7 +104,7 @@ const parse_connect = (connect, config) => { }; const read_from_config_file = (project_path, config_file) => { - const config = { auth: { } }; + const config = {auth: { }}; let fileData, configFilename; @@ -138,7 +138,7 @@ const read_from_config_file = (project_path, config_file) => { }; const read_from_secrets_file = (projectPath, secretsFile) => { - const config = { auth: { } }; + const config = {auth: { }}; let fileData, secretsFilename; @@ -174,7 +174,7 @@ const read_from_secrets_file = (projectPath, secretsFile) => { const env_regex = /^HZ_([A-Z]+([_]?[A-Z]+)*)$/; const read_from_env = () => { - const config = { auth: { } }; + const config = {auth: { }}; for (const env_var in process.env) { const matches = env_regex.exec(env_var); if (matches && matches[1]) { @@ -208,7 +208,7 @@ const read_from_env = () => { // Handles reading configuration from the parsed flags // NOTE: New flags must be manually added here or they will not apply correctly const read_from_flags = (parsed) => { - const config = { auth: { } }; + const config = {auth: { }}; // Dev mode if (parsed.dev) { @@ -293,7 +293,7 @@ const read_from_flags = (parsed) => { if (params.length !== 3) { throw new Error(`Expected --auth PROVIDER,ID,SECRET, but found "${auth_options}"`); } - config.auth[params[0]] = { id: params[1], secret: params[2] }; + config.auth[params[0]] = {id: params[1], secret: params[2]}; }); } diff --git a/cli/src/utils/initialize_joi.js b/cli/src/utils/initialize_joi.js index 049fed3f2..a93d09f32 100644 --- a/cli/src/utils/initialize_joi.js +++ b/cli/src/utils/initialize_joi.js @@ -4,4 +4,4 @@ // This is used because tests will mock the filesystem, and the lazy // `require`s done by joi will no longer work at that point. module.exports = (joi) => - joi.validate('', joi.any().when('', { is: '', then: joi.any() })); + joi.validate('', joi.any().when('', {is: '', then: joi.any()})); diff --git a/cli/src/utils/interrupt.js b/cli/src/utils/interrupt.js index 289e7b626..69a85c885 100644 --- a/cli/src/utils/interrupt.js +++ b/cli/src/utils/interrupt.js @@ -1,6 +1,6 @@ 'use strict'; -const handlers = [ ]; +const handlers = []; const on_interrupt = (cb) => { handlers.push(cb); @@ -24,4 +24,4 @@ const interrupt = () => { process.on('SIGTERM', interrupt); process.on('SIGINT', interrupt); -module.exports = { on_interrupt, interrupt }; +module.exports = {on_interrupt, interrupt}; diff --git a/cli/src/utils/start_rdb_server.js b/cli/src/utils/start_rdb_server.js index 07b893934..bc7d955a6 100644 --- a/cli/src/utils/start_rdb_server.js +++ b/cli/src/utils/start_rdb_server.js @@ -2,6 +2,7 @@ const each_line_in_pipe = require('./each_line_in_pipe'); const horizon_server = require('@horizon/server'); +const {rethinkdbVersionCheck: versionCheck} = require('@horizon/plugins/common/utils'); const execSync = require('child_process').execSync; const spawn = require('child_process').spawn; @@ -13,12 +14,11 @@ const infoLevelLog = (msg) => /^Running/.test(msg) || /^Listening/.test(msg); const r = horizon_server.r; const logger = horizon_server.logger; -const version_check = horizon_server.utils.rethinkdb_version_check; class RethinkdbServer { constructor(options) { const quiet = Boolean(options.quiet); - const bind = options.bind || [ '127.0.0.1' ]; + const bind = options.bind || ['127.0.0.1']; const dataDir = options.dataDir || defaultDatadir; const driverPort = options.rdbPort; const httpPort = options.rdbHttpPort; @@ -30,14 +30,14 @@ class RethinkdbServer { } // Check if RethinkDB is sufficient version for Horizon - version_check(execSync('rethinkdb --version', { timeout: 5000 }).toString()); + versionCheck(execSync('rethinkdb --version', {timeout: 5000}).toString()); - const args = [ '--http-port', String(httpPort || 0), + const args = ['--http-port', String(httpPort || 0), '--cluster-port', '0', '--driver-port', String(driverPort || 0), '--cache-size', String(cacheSize), '--directory', dataDir, - '--no-update-check' ]; + '--no-update-check']; bind.forEach((host) => args.push('--bind', host)); this.proc = spawn('rethinkdb', args); @@ -103,7 +103,7 @@ class RethinkdbServer { // This is only used by tests - cli commands use a more generic method as // the database may be launched elsewhere. connect() { - return r.connect({ host: 'localhost', port: this.driver_port }); + return r.connect({host: 'localhost', port: this.driver_port}); } close() { diff --git a/plugin_router/.babelrc b/plugin-router/.babelrc similarity index 100% rename from plugin_router/.babelrc rename to plugin-router/.babelrc diff --git a/plugin_router/.eslintrc.js b/plugin-router/.eslintrc.js similarity index 100% rename from plugin_router/.eslintrc.js rename to plugin-router/.eslintrc.js diff --git a/plugin_router/package.json b/plugin-router/package.json similarity index 88% rename from plugin_router/package.json rename to plugin-router/package.json index 9034fab2e..547aff22c 100644 --- a/plugin_router/package.json +++ b/plugin-router/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin_router", + "name": "@horizon/plugin-router", "version": "1.0.0", "description": "Plugin router for Horizon.", "main": "src/index.js", @@ -23,6 +23,9 @@ "dependencies": { "toposort-class": "^1.0.1" }, + "peerDependencies": { + "@horizon/server": "3.x" + }, "devDependencies": { "eslint": "^2.3.0", "istanbul": "^0.4.3", diff --git a/plugin_router/src/index.js b/plugin-router/src/index.js similarity index 100% rename from plugin_router/src/index.js rename to plugin-router/src/index.js diff --git a/plugin_router/src/request.js b/plugin-router/src/request.js similarity index 100% rename from plugin_router/src/request.js rename to plugin-router/src/request.js diff --git a/plugins/src/collection.js b/plugins/src/collection.js index 0b7dc57b1..8d179cecb 100644 --- a/plugins/src/collection.js +++ b/plugins/src/collection.js @@ -1,6 +1,6 @@ 'use strict'; -const {ReliableMetadata} = require('./common/metadata.js'); +const {ReliableMetadata} = require('./collection/metadata.js'); function collection(metadata) { return (req, res, next) => { diff --git a/plugins/src/collection/metadata.js b/plugins/src/collection/metadata.js index 55837fa49..672108d71 100644 --- a/plugins/src/collection/metadata.js +++ b/plugins/src/collection/metadata.js @@ -1,7 +1,7 @@ 'use strict'; const Collection = require('./collection').Collection; -const utils = require('./utils'); +const utils = require('../common/utils'); const assert = require('assert'); diff --git a/plugins/src/fetch.js b/plugins/src/fetch.js index bc552e47c..1cdc35663 100644 --- a/plugins/src/fetch.js +++ b/plugins/src/fetch.js @@ -1,6 +1,6 @@ 'use strict'; -const utils = require('./common/utils'); +const {reqlOptions} = require('./common/utils'); const {makeReadReql} = require('./common/reads'); function fetch(ctx) { @@ -15,7 +15,7 @@ function fetch(ctx) { next(new Error('"fetch" requires permissions to run')); } else { makeReadReql(req).then((reql) => - reql.run(conn, utils.reqlOptions) + reql.run(conn, reqlOptions) ).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { res.complete.then(() => { diff --git a/plugins/src/insert.js b/plugins/src/insert.js index ef351a358..215361d3a 100644 --- a/plugins/src/insert.js +++ b/plugins/src/insert.js @@ -1,7 +1,7 @@ 'use strict'; -const utils = require('./common/utils'); -const common = require('./common/writes'); +const {reqlOptions} = require('./common/utils'); +const writes = require('./common/writes'); const {r} = require('@horizon/server'); @@ -18,19 +18,19 @@ function insert(server) { throw new Error('No permissions given for insert operation.'); } - common.retry_loop(request.options.insert, permissions, timeout, + writes.retry_loop(request.options.insert, permissions, timeout, (rows) => // pre-validation, all rows Array(rows.length).fill(null), (validator, row, info) => { // validation, each row if (!validator(request.clientCtx, info, row)) { - return new Error(common.unauthorized_msg); + return new Error(writes.unauthorized_msg); } }, (rows) => // write to database, all valid rows collection.table - .insert(rows.map((row) => common.apply_version(r.expr(row), 0)), + .insert(rows.map((row) => writes.apply_version(r.expr(row), 0)), {returnChanges: 'always'}) - .run(conn, utils.reqlOptions) + .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); }; } diff --git a/plugins/src/permissions.js b/plugins/src/permissions.js index ddfd281ff..9bf007964 100644 --- a/plugins/src/permissions.js +++ b/plugins/src/permissions.js @@ -1,6 +1,6 @@ 'use strict'; -const Rule = require('./rule'); +const Rule = require('./permissions/rule'); const assert = require('assert'); diff --git a/plugins/src/permissions/template.js b/plugins/src/permissions/template.js index caa530c9e..23fbae2db 100644 --- a/plugins/src/permissions/template.js +++ b/plugins/src/permissions/template.js @@ -1,6 +1,6 @@ 'use strict'; -const remake_error = require('./remake_error'); +const {remakeError} = require('../common/utils'); const assert = require('assert'); const vm = require('vm'); @@ -162,7 +162,7 @@ const make_template = (str) => { const sandbox = Object.assign({}, env); return vm.runInNewContext(str, sandbox); } catch (err) { - throw remake_error(err); + throw remakeError(err); } }; diff --git a/plugins/src/permissions/validator.js b/plugins/src/permissions/validator.js index 933118d36..1620df4c1 100644 --- a/plugins/src/permissions/validator.js +++ b/plugins/src/permissions/validator.js @@ -1,6 +1,6 @@ 'use strict'; -const remake_error = require('./remake_error'); +const {remakeError} = require('../common/utils'); const assert = require('assert'); const vm = require('vm'); @@ -10,7 +10,7 @@ class Validator { try { this._fn = vm.runInNewContext(str, { }); } catch (err) { - throw remake_error(err); + throw remakeError(err); } assert(typeof this._fn === 'function'); } diff --git a/plugins/src/remove.js b/plugins/src/remove.js index d0e41dc99..3a8ccc7db 100644 --- a/plugins/src/remove.js +++ b/plugins/src/remove.js @@ -1,8 +1,7 @@ 'use strict'; -const utils = require('./common/utils'); -const common = require('./common/writes'); -const hz_v = utils.versionField; +const {reqlOptions, versionField: hz_v} = require('./common/utils'); +const writes = require('./common/writes'); const {r} = require('@horizon/server'); @@ -19,13 +18,13 @@ function remove(server) { throw new Error('No permissions given for insert operation.'); } - common.retry_loop(request.options.remove, permissions, timeout, + writes.retry_loop(request.options.remove, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows.map((row) => row.id)) .map((id) => collection.table.get(id)) - .run(conn, utils.reqlOptions), + .run(conn, reqlOptions), (validator, row, info) => - common.validate_old_row_required(validator, request.clientCtx, row, info, null), + writes.validate_old_row_required(validator, request.clientCtx, row, info, null), (rows) => // write to database, all valid rows r.expr(rows).do((row_data) => row_data.forEach((info) => @@ -37,7 +36,7 @@ function remove(server) { // The row may have been changed between the get and now r.and(info.hasFields(hz_v), row(hz_v).default(-1).ne(info(hz_v))), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise, we can safely remove the row null), @@ -52,7 +51,7 @@ function remove(server) { {old_val: {id: row_data(index)('id')}}), res('changes')(index))).coerceTo('array'), }))) - .run(conn, utils.reqlOptions) + .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); }; } diff --git a/plugins/src/replace.js b/plugins/src/replace.js index 2ea5b2d19..0fc05e3d0 100644 --- a/plugins/src/replace.js +++ b/plugins/src/replace.js @@ -1,8 +1,7 @@ 'use strict'; -const utils = require('./common/utils'); -const common = require('./common/writes'); -const hz_v = utils.versionField; +const {reqlOptions, versionField: hz_v} = require('./common/utils'); +const writes = require('./common/writes'); const {r} = require('@horizon/server'); @@ -19,31 +18,31 @@ function replace(server) { throw new Error('No permissions given for insert operation.'); } - common.retry_loop(request.options.replace, permissions, timeout, + writes.retry_loop(request.options.replace, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows.map((row) => row.id)) .map((id) => collection.table.get(id)) - .run(conn, utils.reqlOptions), + .run(conn, reqlOptions), (validator, row, info) => - common.validate_old_row_required(validator, request.clientCtx, row, info, row), + writes.validate_old_row_required(validator, request.clientCtx, row, info, row), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => collection.table.get(new_row('id')).replace((old_row) => r.branch(// The row may have been deleted between the get and now old_row.eq(null), - r.error(common.missing_msg), + r.error(writes.missing_msg), // The row may have been changed between the get and now r.and(new_row.hasFields(hz_v), old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise, we can safely replace the row - common.apply_version( + writes.apply_version( new_row, old_row(hz_v).default(-1).add(1))), {returnChanges: 'always'})) - .run(conn, utils.reqlOptions) + .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); }; } diff --git a/plugins/src/store.js b/plugins/src/store.js index fecc90e03..875a1f2dd 100644 --- a/plugins/src/store.js +++ b/plugins/src/store.js @@ -1,8 +1,7 @@ 'use strict'; -const utils = require('./common/utils'); -const common = require('./common/writes'); -const hz_v = utils.versionField; +const {reqlOptions, versionField: hz_v} = require('./common/utils'); +const writes = require('./common/writes'); const {r} = require('@horizon/server'); @@ -19,13 +18,13 @@ function store(ctx) { throw new Error('No permissions given for insert operation.'); } - common.retry_loop(request.options.store, permissions, timeout, + writes.retry_loop(request.options.store, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows.map((row) => (row.id === undefined ? null : row.id))) .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) - .run(conn, utils.reqlOptions), + .run(conn, reqlOptions), (validator, row, info) => - common.validate_old_row_optional(validator, request.clientCtx, row, info, row), + writes.validate_old_row_optional(validator, request.clientCtx, row, info, row), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => @@ -36,27 +35,27 @@ function store(ctx) { r.branch( // Error if we were expecting the row to exist new_row.hasFields(hz_v), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise, insert the row - common.apply_version(new_row, 0) + writes.apply_version(new_row, 0) ), r.branch( // The row may have changed from the expected version r.and(new_row.hasFields(hz_v), old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise, we can overwrite the row - common.apply_version(new_row, + writes.apply_version(new_row, old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row does not have an id, so it will autogenerate - collection.table.insert(common.apply_version(new_row, 0), + collection.table.insert(writes.apply_version(new_row, 0), {returnChanges: 'always'}))) - .run(conn, utils.reqlOptions) + .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); }; } diff --git a/plugins/src/update.js b/plugins/src/update.js index e0954f274..219c99adb 100644 --- a/plugins/src/update.js +++ b/plugins/src/update.js @@ -1,8 +1,7 @@ 'use strict'; -const utils = require('./common/utils'); -const common = require('./common/writes'); -const hz_v = utils.versionField; +const {reqlOptions, versionField: hz_v} = require('./common/utils'); +const writes = require('./common/writes'); const {r} = require('@horizon/server'); @@ -19,7 +18,7 @@ function update(server) { throw new Error('No permissions given for insert operation.'); } - common.retry_loop(request.options.update, permissions, timeout, + writes.retry_loop(request.options.update, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows) .map((new_row) => @@ -27,9 +26,9 @@ function update(server) { r.branch(old_row.eq(null), null, [old_row, old_row.merge(new_row)]))) - .run(conn, utils.reqlOptions), + .run(conn, reqlOptions), (validator, row, info) => - common.validate_old_row_required( + writes.validate_old_row_required( validator, request.clientCtx, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) @@ -37,18 +36,18 @@ function update(server) { collection.table.get(new_row('id')).replace((old_row) => r.branch(// The row may have been deleted between the get and now old_row.eq(null), - r.error(common.missing_msg), + r.error(writes.missing_msg), // The row may have been changed between the get and now r.and(new_row.hasFields(hz_v), old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise we can update the row and increment the version - common.apply_version(old_row.merge(new_row), + writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1))), {returnChanges: 'always'})) - .run(conn, utils.reqlOptions) + .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); }; } diff --git a/plugins/src/upsert.js b/plugins/src/upsert.js index 9a4373b94..e407a4187 100644 --- a/plugins/src/upsert.js +++ b/plugins/src/upsert.js @@ -1,8 +1,7 @@ 'use strict'; -const utils = require('./common/utils'); -const common = require('./common/writes'); -const hz_v = utils.versionField; +const {reqlOptions, versionField: hz_v} = require('./common/utils'); +const writes = require('./common/writes'); const {r} = require('@horizon/server'); @@ -19,7 +18,7 @@ function upsert(server) { throw new Error('No permissions given for insert operation.'); } - common.retry_loop(request.options.upsert, permissions, timeout, + writes.retry_loop(request.options.upsert, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows) .map((new_row) => @@ -29,9 +28,9 @@ function upsert(server) { [null, new_row], [old_row, old_row.merge(new_row)])), [null, new_row])) - .run(conn, utils.reqlOptions), + .run(conn, reqlOptions), (validator, row, info) => - common.validate_old_row_optional( + writes.validate_old_row_optional( validator, request.clientCtx, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) @@ -43,27 +42,27 @@ function upsert(server) { r.branch( // Error if we were expecting the row to exist new_row.hasFields(hz_v), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise, insert the row - common.apply_version(new_row, 0) + writes.apply_version(new_row, 0) ), r.branch( // The row may have changed from the expected version r.and(new_row.hasFields(hz_v), old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(common.invalidated_msg), + r.error(writes.invalidated_msg), // Otherwise, we can update the row and version - common.apply_version(old_row.merge(new_row), + writes.apply_version(old_row.merge(new_row), old_row(hz_v).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row did not have an id, so it will autogenerate - collection.table.insert(common.apply_version(new_row, 0), + collection.table.insert(writes.apply_version(new_row, 0), {returnChanges: 'always'}))) - .run(conn, utils.reqlOptions) + .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); }; } diff --git a/plugins/src/watch.js b/plugins/src/watch.js index 96d357925..f07cc0e3c 100644 --- a/plugins/src/watch.js +++ b/plugins/src/watch.js @@ -1,6 +1,6 @@ 'use strict'; -const utils = require('./common/utils'); +const {reqlOptions} = require('./common/utils'); const {makeReadReql} = require('./common/reads'); function watch(server) { @@ -22,7 +22,7 @@ function watch(server) { include_offsets: req.getParameter('order') !== undefined && req.getParameter('limit') !== undefined, - }).run(conn, utils.reqlOptions) + }).run(conn, reqlOptions) ).then((feed) => { res.complete.then(() => { feed.close().catch(() => { }); diff --git a/test/package.json b/test/package.json new file mode 100644 index 000000000..49047b8da --- /dev/null +++ b/test/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/server-test", + "version": "1.0.0", + "description": "Embedded horizon server for running tests.", + "main": "dist/main.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/cli": "3.0.0", + "@horizon/plugin-router": "1.0.0", + "@horizon/plugins": "1.0.0", + "@horizon/server": "3.0.0", + "argparse": "^1.0.7" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/test/serve.js b/test/serve.js index a05519493..e7e479a76 100755 --- a/test/serve.js +++ b/test/serve.js @@ -5,23 +5,15 @@ require('../server/node_modules/source-map-support').install(); Error.stackTraceLimit = Infinity; -const horizon = require('../server'); -const PluginRouter = require('../plugin_router'); -const permit_all = require('../plugins/permit_all'); -const reads = require('../plugins/reads'); -const writes = require('../plugins/writes'); -const collection = require('../plugins/collection').default; -const timeout = require('../plugins/timeout'); +const horizon_server = require('@horizon/server'); +const PluginRouter = require('@horizon/plugin-router'); +const horizon_plugins = require('@horizon/plugins'); // Utilities provided by the CLI library -const each_line_in_pipe = require('../cli/src/utils/each_line_in_pipe'); -const start_rdb_server = require('../cli/src/utils/start_rdb_server'); -const rm_sync_recursive = require('../cli/src/utils/rm_sync_recursive'); -const parse_yes_no_option = require('../cli/src/utils/parse_yes_no_option'); - -// We could make this a module, but we already require the server to be configured, -// so reuse its argparse module -const argparse = require('../cli/node_modules/argparse'); +const each_line_in_pipe = require('@horizon/cli/src/utils/each_line_in_pipe'); +const start_rdb_server = require('@horizon/cli/src/utils/start_rdb_server'); +const rm_sync_recursive = require('@horizon/cli/src/utils/rm_sync_recursive'); +const parse_yes_no_option = require('@horizon/cli/src/utils/parse_yes_no_option'); const assert = require('assert'); const child_process = require('child_process'); @@ -33,6 +25,8 @@ const url = require('url'); const process = require('process'); const crypto = require('crypto'); +const argparse = require('argparse'); + const data_dir = path.resolve(__dirname, 'rethinkdb_data_test'); const test_dist_dir = path.resolve(__dirname, '../client/dist'); const examples_dir = path.resolve(__dirname, '../examples'); @@ -173,8 +167,8 @@ new Promise((resolve) => { console.log(`RethinkDB server listening for HTTP on port ${server.http_port}.`); console.log('starting horizon'); - horizon.logger.level = 'debug'; - const horizon_server = new horizon.Server(http_servers, { + horizon_server.logger.level = 'debug'; + const hz_server = new horizon_server.Server(http_servers, { rdb_port: server.driver_port, project_name: 'hz_test', auth: { @@ -185,23 +179,35 @@ new Promise((resolve) => { }); console.log('starting http servers'); - const plugins = new PluginRouter(horizon_server); + const plugins = new PluginRouter(hz_server); Promise.all([ - plugins.add(permit_all()), - plugins.add(collection({ + plugins.add(plugins.collection({ auto_create_collection: true, auto_create_index: true, })), - plugins.add(timeout()), - plugins.add(reads()), - plugins.add(writes()), + plugins.add(plugins.permit_all()), + plugins.add(plugins.timeout()), + plugins.add(plugins.insert()), + plugins.add(plugins.store()), + plugins.add(plugins.update()), + plugins.add(plugins.upsert()), + plugins.add(plugins.remove()), + plugins.add(plugins.replace()), + plugins.add(plugins.fetch()), + plugins.add(plugins.watch()), + plugins.add(plugins.above()), + plugins.add(plugins.below()), + plugins.add(plugins.order()), + plugins.add(plugins.limit()), + plugins.add(plugins.find()), + plugins.add(plugins.findAll()), ]).catch((err) => console.log(`Plugin initialization failed: ${err.stack}`) ); plugins.once('ready', () => { console.log('READY OMGZZZZ'); - horizon_server.set_middleware(plugins.hzMiddleware()); + hz_server.set_middleware(plugins.hzMiddleware()); // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server http_servers.forEach((serv, i) => { From f948716d4da21192c75b27ad429593cea4ad57c0 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 04:31:02 -0700 Subject: [PATCH 52/86] fixing es6 compat --- cli/src/utils/start_rdb_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/utils/start_rdb_server.js b/cli/src/utils/start_rdb_server.js index bc7d955a6..c9e02ba0c 100644 --- a/cli/src/utils/start_rdb_server.js +++ b/cli/src/utils/start_rdb_server.js @@ -2,7 +2,7 @@ const each_line_in_pipe = require('./each_line_in_pipe'); const horizon_server = require('@horizon/server'); -const {rethinkdbVersionCheck: versionCheck} = require('@horizon/plugins/common/utils'); +const versionCheck = require('@horizon/plugins/common/utils').rethinkdbVersionCheck; const execSync = require('child_process').execSync; const spawn = require('child_process').spawn; From 12846c2c0d68bbf3c37a5c6cce2980357055faf9 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 05:54:53 -0700 Subject: [PATCH 53/86] broke plugins up into separate modules --- plugins/{ => above}/.babelrc | 0 plugins/above/package.json | 38 +++++++++++++++++++ plugins/{ => above}/src/above.js | 2 +- plugins/below/.babelrc | 3 ++ plugins/below/package.json | 38 +++++++++++++++++++ plugins/{ => below}/src/below.js | 2 +- plugins/collection/.babelrc | 3 ++ plugins/collection/package.json | 38 +++++++++++++++++++ plugins/{ => collection}/src/collection.js | 0 .../src/collection/collection.js | 0 .../{ => collection}/src/collection/index.js | 0 .../src/collection/metadata.js | 18 +++++---- .../{ => collection}/src/collection/table.js | 0 plugins/defaults/.babelrc | 3 ++ plugins/defaults/package.json | 37 ++++++++++++++++++ .../index.js => defaults/src/defaults.js} | 0 plugins/fetch/.babelrc | 3 ++ plugins/fetch/package.json | 38 +++++++++++++++++++ plugins/{ => fetch}/src/fetch.js | 5 +-- plugins/find/.babelrc | 3 ++ plugins/find/package.json | 38 +++++++++++++++++++ plugins/{ => find}/src/find.js | 2 +- plugins/findAll/.babelrc | 3 ++ plugins/findAll/package.json | 38 +++++++++++++++++++ plugins/{ => findAll}/src/findAll.js | 2 +- plugins/insert/.babelrc | 3 ++ plugins/insert/package.json | 38 +++++++++++++++++++ plugins/{ => insert}/src/insert.js | 4 +- plugins/limit/.babelrc | 3 ++ plugins/limit/package.json | 37 ++++++++++++++++++ plugins/{ => limit}/src/limit.js | 0 plugins/order/.babelrc | 3 ++ plugins/order/package.json | 37 ++++++++++++++++++ plugins/{ => order}/src/order.js | 0 plugins/permissions/.babelrc | 3 ++ plugins/permissions/package.json | 38 +++++++++++++++++++ .../permissions => permissions/src}/group.js | 0 plugins/{ => permissions}/src/permissions.js | 2 +- .../permissions => permissions/src}/rule.js | 1 + .../src}/template.js | 3 +- .../src}/validator.js | 4 +- plugins/permit-all/.babelrc | 3 ++ plugins/permit-all/package.json | 37 ++++++++++++++++++ .../src/permit-all.js} | 0 plugins/remove/.babelrc | 3 ++ plugins/remove/package.json | 38 +++++++++++++++++++ plugins/{ => remove}/src/remove.js | 4 +- plugins/replace/.babelrc | 3 ++ plugins/replace/package.json | 38 +++++++++++++++++++ plugins/{ => replace}/src/replace.js | 4 +- plugins/store/.babelrc | 3 ++ plugins/store/package.json | 38 +++++++++++++++++++ plugins/{ => store}/src/store.js | 4 +- plugins/timeout/.babelrc | 3 ++ plugins/timeout/package.json | 37 ++++++++++++++++++ plugins/{ => timeout}/src/timeout.js | 0 plugins/update/.babelrc | 3 ++ plugins/update/package.json | 38 +++++++++++++++++++ plugins/{ => update}/src/update.js | 4 +- plugins/upsert/.babelrc | 3 ++ plugins/upsert/package.json | 38 +++++++++++++++++++ plugins/{ => upsert}/src/upsert.js | 4 +- plugins/utils/.babelrc | 3 ++ plugins/{ => utils}/package.json | 6 +-- plugins/{src/common => utils/src}/reads.js | 0 plugins/{src/common => utils/src}/utils.js | 15 ++++++-- plugins/{src/common => utils/src}/writes.js | 0 plugins/watch/.babelrc | 3 ++ plugins/watch/package.json | 38 +++++++++++++++++++ plugins/{ => watch}/src/watch.js | 5 +-- 70 files changed, 817 insertions(+), 48 deletions(-) rename plugins/{ => above}/.babelrc (100%) create mode 100644 plugins/above/package.json rename plugins/{ => above}/src/above.js (93%) create mode 100644 plugins/below/.babelrc create mode 100644 plugins/below/package.json rename plugins/{ => below}/src/below.js (93%) create mode 100644 plugins/collection/.babelrc create mode 100644 plugins/collection/package.json rename plugins/{ => collection}/src/collection.js (100%) rename plugins/{ => collection}/src/collection/collection.js (100%) rename plugins/{ => collection}/src/collection/index.js (100%) rename plugins/{ => collection}/src/collection/metadata.js (96%) rename plugins/{ => collection}/src/collection/table.js (100%) create mode 100644 plugins/defaults/.babelrc create mode 100644 plugins/defaults/package.json rename plugins/{src/index.js => defaults/src/defaults.js} (100%) create mode 100644 plugins/fetch/.babelrc create mode 100644 plugins/fetch/package.json rename plugins/{ => fetch}/src/fetch.js (93%) create mode 100644 plugins/find/.babelrc create mode 100644 plugins/find/package.json rename plugins/{ => find}/src/find.js (90%) create mode 100644 plugins/findAll/.babelrc create mode 100644 plugins/findAll/package.json rename plugins/{ => findAll}/src/findAll.js (91%) create mode 100644 plugins/insert/.babelrc create mode 100644 plugins/insert/package.json rename plugins/{ => insert}/src/insert.js (93%) create mode 100644 plugins/limit/.babelrc create mode 100644 plugins/limit/package.json rename plugins/{ => limit}/src/limit.js (100%) create mode 100644 plugins/order/.babelrc create mode 100644 plugins/order/package.json rename plugins/{ => order}/src/order.js (100%) create mode 100644 plugins/permissions/.babelrc create mode 100644 plugins/permissions/package.json rename plugins/{src/permissions => permissions/src}/group.js (100%) rename plugins/{ => permissions}/src/permissions.js (99%) rename plugins/{src/permissions => permissions/src}/rule.js (99%) rename plugins/{src/permissions => permissions/src}/template.js (99%) rename plugins/{src/permissions => permissions/src}/validator.js (86%) create mode 100644 plugins/permit-all/.babelrc create mode 100644 plugins/permit-all/package.json rename plugins/{src/permit_all.js => permit-all/src/permit-all.js} (100%) create mode 100644 plugins/remove/.babelrc create mode 100644 plugins/remove/package.json rename plugins/{ => remove}/src/remove.js (95%) create mode 100644 plugins/replace/.babelrc create mode 100644 plugins/replace/package.json rename plugins/{ => replace}/src/replace.js (94%) create mode 100644 plugins/store/.babelrc create mode 100644 plugins/store/package.json rename plugins/{ => store}/src/store.js (96%) create mode 100644 plugins/timeout/.babelrc create mode 100644 plugins/timeout/package.json rename plugins/{ => timeout}/src/timeout.js (100%) create mode 100644 plugins/update/.babelrc create mode 100644 plugins/update/package.json rename plugins/{ => update}/src/update.js (95%) create mode 100644 plugins/upsert/.babelrc create mode 100644 plugins/upsert/package.json rename plugins/{ => upsert}/src/upsert.js (96%) create mode 100644 plugins/utils/.babelrc rename plugins/{ => utils}/package.json (84%) rename plugins/{src/common => utils/src}/reads.js (100%) rename plugins/{src/common => utils/src}/utils.js (78%) rename plugins/{src/common => utils/src}/writes.js (100%) create mode 100644 plugins/watch/.babelrc create mode 100644 plugins/watch/package.json rename plugins/{ => watch}/src/watch.js (92%) diff --git a/plugins/.babelrc b/plugins/above/.babelrc similarity index 100% rename from plugins/.babelrc rename to plugins/above/.babelrc diff --git a/plugins/above/package.json b/plugins/above/package.json new file mode 100644 index 000000000..7fcf4c666 --- /dev/null +++ b/plugins/above/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugin-above", + "version": "1.0.0", + "description": "Plugin for the 'above' term in the Horizon Collections API.", + "main": "dist/above.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/above.js b/plugins/above/src/above.js similarity index 93% rename from plugins/src/above.js rename to plugins/above/src/above.js index 1a8922fb0..494a6cba3 100644 --- a/plugins/src/above.js +++ b/plugins/above/src/above.js @@ -1,6 +1,6 @@ 'use strict'; -const {isObject} = require('./common/utils'); +const {isObject} = require('@horizon/plugin-utils'); function above(req, res, next) { const args = req.options.above; diff --git a/plugins/below/.babelrc b/plugins/below/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/below/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/below/package.json b/plugins/below/package.json new file mode 100644 index 000000000..6b3115da0 --- /dev/null +++ b/plugins/below/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugin-below", + "version": "1.0.0", + "description": "Plugin for the 'below' term in the Horizon Collections API.", + "main": "dist/below.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/below.js b/plugins/below/src/below.js similarity index 93% rename from plugins/src/below.js rename to plugins/below/src/below.js index fcb17e2ba..79b7f9630 100644 --- a/plugins/src/below.js +++ b/plugins/below/src/below.js @@ -1,6 +1,6 @@ 'use strict'; -const {isObject} = require('./common/utils'); +const {isObject} = require('@horizon/plugin-utils'); function below(req, res, next) { const args = req.options.below; diff --git a/plugins/collection/.babelrc b/plugins/collection/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/collection/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/collection/package.json b/plugins/collection/package.json new file mode 100644 index 000000000..535789c74 --- /dev/null +++ b/plugins/collection/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-collection", + "version": "1.0.0", + "description": "Plugin for the 'collection' term in the Horizon Collections API.", + "main": "dist/collection.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/collection.js b/plugins/collection/src/collection.js similarity index 100% rename from plugins/src/collection.js rename to plugins/collection/src/collection.js diff --git a/plugins/src/collection/collection.js b/plugins/collection/src/collection/collection.js similarity index 100% rename from plugins/src/collection/collection.js rename to plugins/collection/src/collection/collection.js diff --git a/plugins/src/collection/index.js b/plugins/collection/src/collection/index.js similarity index 100% rename from plugins/src/collection/index.js rename to plugins/collection/src/collection/index.js diff --git a/plugins/src/collection/metadata.js b/plugins/collection/src/collection/metadata.js similarity index 96% rename from plugins/src/collection/metadata.js rename to plugins/collection/src/collection/metadata.js index 672108d71..a311c692a 100644 --- a/plugins/src/collection/metadata.js +++ b/plugins/collection/src/collection/metadata.js @@ -1,16 +1,16 @@ 'use strict'; const Collection = require('./collection').Collection; -const utils = require('../common/utils'); const assert = require('assert'); const {r, logger, Reliable, ReliableUnion} = require('@horizon/server'); +const {rethinkdbVersionCheck} = require('@horizon/plugin-utils'); -const metadata_version = [2, 0, 0]; +const metadataVersion = [2, 0, 0]; -const createCollection = (db, name, conn) => - r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => +function createCollection(db, name, conn) { + return r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => r.branch( res('errors').ne(0), r.error(res('first_error')), @@ -19,9 +19,10 @@ const createCollection = (db, name, conn) => res ) ).run(conn); +} -const initializeMetadata = (db, conn) => - r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) +function initializeMetadata(db, conn) { + return r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) .then(() => Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => r.branch(r.db(db).tableList().contains(table), @@ -35,10 +36,11 @@ const initializeMetadata = (db, conn) => r.db(db).tableList().contains('users').not().run(conn).then(() => createCollection(r, db, 'users', conn)), r.db(db).table('hz_collections') - .insert({id: 'hz_metadata', version: metadata_version}) + .insert({id: 'hz_metadata', version: metadataVersion}) .run(conn), ]) ); +} class StaleAttemptError extends Error { } @@ -73,7 +75,7 @@ class ReliableInit extends Reliable { this.check_attempt(attempt); logger.debug('checking rethinkdb version'); const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version'); - return q.run(conn).then((res) => utils.rethinkdbVersionCheck(res)); + return q.run(conn).then((res) => rethinkdbVersionCheck(res)); }).then(() => { this.check_attempt(attempt); logger.debug('checking for old metadata version'); diff --git a/plugins/src/collection/table.js b/plugins/collection/src/collection/table.js similarity index 100% rename from plugins/src/collection/table.js rename to plugins/collection/src/collection/table.js diff --git a/plugins/defaults/.babelrc b/plugins/defaults/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/defaults/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json new file mode 100644 index 000000000..f3c263a86 --- /dev/null +++ b/plugins/defaults/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugin-defaults", + "version": "1.0.0", + "description": "Contains all the default plugins for the Horizon Collections API.", + "main": "dist/defaults.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/index.js b/plugins/defaults/src/defaults.js similarity index 100% rename from plugins/src/index.js rename to plugins/defaults/src/defaults.js diff --git a/plugins/fetch/.babelrc b/plugins/fetch/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/fetch/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/fetch/package.json b/plugins/fetch/package.json new file mode 100644 index 000000000..34ed102d3 --- /dev/null +++ b/plugins/fetch/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-fetch", + "version": "1.0.0", + "description": "Plugin for the 'fetch' term in the Horizon Collections API.", + "main": "dist/fetch.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/fetch.js b/plugins/fetch/src/fetch.js similarity index 93% rename from plugins/src/fetch.js rename to plugins/fetch/src/fetch.js index 1cdc35663..4f8d3d317 100644 --- a/plugins/src/fetch.js +++ b/plugins/fetch/src/fetch.js @@ -1,7 +1,6 @@ 'use strict'; -const {reqlOptions} = require('./common/utils'); -const {makeReadReql} = require('./common/reads'); +const {reqlOptions, reads} = require('@horizon/plugin-utils'); function fetch(ctx) { return (req, res, next) => { @@ -14,7 +13,7 @@ function fetch(ctx) { } else if (!permissions) { next(new Error('"fetch" requires permissions to run')); } else { - makeReadReql(req).then((reql) => + reads.makeReadReql(req).then((reql) => reql.run(conn, reqlOptions) ).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { diff --git a/plugins/find/.babelrc b/plugins/find/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/find/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/find/package.json b/plugins/find/package.json new file mode 100644 index 000000000..ed9f753dd --- /dev/null +++ b/plugins/find/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-find", + "version": "1.0.0", + "description": "Plugin for the 'find' term in the Horizon Collections API.", + "main": "dist/find.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/find.js b/plugins/find/src/find.js similarity index 90% rename from plugins/src/find.js rename to plugins/find/src/find.js index d55d7d8e4..c1e0f5a0a 100644 --- a/plugins/src/find.js +++ b/plugins/find/src/find.js @@ -1,6 +1,6 @@ 'use strict'; -const {isObject} = require('./common/utils'); +const {isObject} = require('@horizon/plugin-utils'); function find(req, res, next) { const args = req.options.find; diff --git a/plugins/findAll/.babelrc b/plugins/findAll/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/findAll/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/findAll/package.json b/plugins/findAll/package.json new file mode 100644 index 000000000..41cbf13ad --- /dev/null +++ b/plugins/findAll/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-findAll", + "version": "1.0.0", + "description": "Plugin for the 'findAll' term in the Horizon Collections API.", + "main": "dist/findAll.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/findAll.js b/plugins/findAll/src/findAll.js similarity index 91% rename from plugins/src/findAll.js rename to plugins/findAll/src/findAll.js index ff2fa20d2..2aaac0bbe 100644 --- a/plugins/src/findAll.js +++ b/plugins/findAll/src/findAll.js @@ -1,6 +1,6 @@ 'use strict'; -const {isObject} = require('./common/utils'); +const {isObject} = require('@horizon/plugin-utils'); function findAll(req, res, next) { const args = req.options.findAll; diff --git a/plugins/insert/.babelrc b/plugins/insert/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/insert/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/insert/package.json b/plugins/insert/package.json new file mode 100644 index 000000000..c80c2e0f0 --- /dev/null +++ b/plugins/insert/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-insert", + "version": "1.0.0", + "description": "Plugin for the 'insert' term in the Horizon Collections API.", + "main": "dist/insert.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/insert.js b/plugins/insert/src/insert.js similarity index 93% rename from plugins/src/insert.js rename to plugins/insert/src/insert.js index 215361d3a..3474f6a3f 100644 --- a/plugins/src/insert.js +++ b/plugins/insert/src/insert.js @@ -1,9 +1,7 @@ 'use strict'; -const {reqlOptions} = require('./common/utils'); -const writes = require('./common/writes'); - const {r} = require('@horizon/server'); +const {reqlOptions, writes} = require('@horizon/plugin-utils'); function insert(server) { return (request, response, next) => { diff --git a/plugins/limit/.babelrc b/plugins/limit/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/limit/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/limit/package.json b/plugins/limit/package.json new file mode 100644 index 000000000..8b89df64c --- /dev/null +++ b/plugins/limit/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugins-limit", + "version": "1.0.0", + "description": "Plugin for the 'limit' term in the Horizon Collections API.", + "main": "dist/limit.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/limit.js b/plugins/limit/src/limit.js similarity index 100% rename from plugins/src/limit.js rename to plugins/limit/src/limit.js diff --git a/plugins/order/.babelrc b/plugins/order/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/order/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/order/package.json b/plugins/order/package.json new file mode 100644 index 000000000..f4d046802 --- /dev/null +++ b/plugins/order/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugins-order", + "version": "1.0.0", + "description": "Plugin for the 'order' term in the Horizon Collections API.", + "main": "dist/order.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/order.js b/plugins/order/src/order.js similarity index 100% rename from plugins/src/order.js rename to plugins/order/src/order.js diff --git a/plugins/permissions/.babelrc b/plugins/permissions/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/permissions/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json new file mode 100644 index 000000000..b6ae84880 --- /dev/null +++ b/plugins/permissions/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-permissions", + "version": "1.0.0", + "description": "Plugin adding user groups and permissions to Horizon.", + "main": "dist/permissions.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/permissions/group.js b/plugins/permissions/src/group.js similarity index 100% rename from plugins/src/permissions/group.js rename to plugins/permissions/src/group.js diff --git a/plugins/src/permissions.js b/plugins/permissions/src/permissions.js similarity index 99% rename from plugins/src/permissions.js rename to plugins/permissions/src/permissions.js index 9bf007964..ddfd281ff 100644 --- a/plugins/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -1,6 +1,6 @@ 'use strict'; -const Rule = require('./permissions/rule'); +const Rule = require('./rule'); const assert = require('assert'); diff --git a/plugins/src/permissions/rule.js b/plugins/permissions/src/rule.js similarity index 99% rename from plugins/src/permissions/rule.js rename to plugins/permissions/src/rule.js index 608c6bc5e..f24175c9d 100644 --- a/plugins/src/permissions/rule.js +++ b/plugins/permissions/src/rule.js @@ -1,4 +1,5 @@ 'use strict'; + const Template = require('./template').Template; const Validator = require('./validator').Validator; diff --git a/plugins/src/permissions/template.js b/plugins/permissions/src/template.js similarity index 99% rename from plugins/src/permissions/template.js rename to plugins/permissions/src/template.js index 23fbae2db..03ed3257a 100644 --- a/plugins/src/permissions/template.js +++ b/plugins/permissions/src/template.js @@ -1,12 +1,11 @@ 'use strict'; -const {remakeError} = require('../common/utils'); - const assert = require('assert'); const vm = require('vm'); const ast = require('@horizon/client/lib/ast'); const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; +const {remakeError} = require('@horizon/plugin-utils'); class Any { constructor(values) { diff --git a/plugins/src/permissions/validator.js b/plugins/permissions/src/validator.js similarity index 86% rename from plugins/src/permissions/validator.js rename to plugins/permissions/src/validator.js index 1620df4c1..e9d54f788 100644 --- a/plugins/src/permissions/validator.js +++ b/plugins/permissions/src/validator.js @@ -1,10 +1,10 @@ 'use strict'; -const {remakeError} = require('../common/utils'); - const assert = require('assert'); const vm = require('vm'); +const {remakeError} = require('@horizon/plugin-utils'); + class Validator { constructor(str) { try { diff --git a/plugins/permit-all/.babelrc b/plugins/permit-all/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/permit-all/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/permit-all/package.json b/plugins/permit-all/package.json new file mode 100644 index 000000000..a306c896a --- /dev/null +++ b/plugins/permit-all/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugins-permit-all", + "version": "1.0.0", + "description": "Plugin allowing any request to run without permissions in Horizon.", + "main": "dist/permit-all.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/permit_all.js b/plugins/permit-all/src/permit-all.js similarity index 100% rename from plugins/src/permit_all.js rename to plugins/permit-all/src/permit-all.js diff --git a/plugins/remove/.babelrc b/plugins/remove/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/remove/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/remove/package.json b/plugins/remove/package.json new file mode 100644 index 000000000..ee2ece9db --- /dev/null +++ b/plugins/remove/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-remove", + "version": "1.0.0", + "description": "Plugin for the 'remove' term in the Horizon Collections API.", + "main": "dist/remove.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/remove.js b/plugins/remove/src/remove.js similarity index 95% rename from plugins/src/remove.js rename to plugins/remove/src/remove.js index 3a8ccc7db..f6a4e3efe 100644 --- a/plugins/src/remove.js +++ b/plugins/remove/src/remove.js @@ -1,9 +1,7 @@ 'use strict'; -const {reqlOptions, versionField: hz_v} = require('./common/utils'); -const writes = require('./common/writes'); - const {r} = require('@horizon/server'); +const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); function remove(server) { return (request, response, next) => { diff --git a/plugins/replace/.babelrc b/plugins/replace/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/replace/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/replace/package.json b/plugins/replace/package.json new file mode 100644 index 000000000..19afdbec2 --- /dev/null +++ b/plugins/replace/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-replace", + "version": "1.0.0", + "description": "Plugin for the 'replace' term in the Horizon Collections API.", + "main": "dist/replace.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/replace.js b/plugins/replace/src/replace.js similarity index 94% rename from plugins/src/replace.js rename to plugins/replace/src/replace.js index 0fc05e3d0..ad33276de 100644 --- a/plugins/src/replace.js +++ b/plugins/replace/src/replace.js @@ -1,9 +1,7 @@ 'use strict'; -const {reqlOptions, versionField: hz_v} = require('./common/utils'); -const writes = require('./common/writes'); - const {r} = require('@horizon/server'); +const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); function replace(server) { return (request, response, next) => { diff --git a/plugins/store/.babelrc b/plugins/store/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/store/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/store/package.json b/plugins/store/package.json new file mode 100644 index 000000000..a91b29535 --- /dev/null +++ b/plugins/store/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-store", + "version": "1.0.0", + "description": "Plugin for the 'store' term in the Horizon Collections API.", + "main": "dist/store.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/store.js b/plugins/store/src/store.js similarity index 96% rename from plugins/src/store.js rename to plugins/store/src/store.js index 875a1f2dd..c6ed97685 100644 --- a/plugins/src/store.js +++ b/plugins/store/src/store.js @@ -1,9 +1,7 @@ 'use strict'; -const {reqlOptions, versionField: hz_v} = require('./common/utils'); -const writes = require('./common/writes'); - const {r} = require('@horizon/server'); +const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); function store(ctx) { return (request, response, next) => { diff --git a/plugins/timeout/.babelrc b/plugins/timeout/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/timeout/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json new file mode 100644 index 000000000..e26b96748 --- /dev/null +++ b/plugins/timeout/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugins-timeout", + "version": "1.0.0", + "description": "Plugin for the 'timeout' term in the Horizon Collections API.", + "main": "dist/timeout.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/timeout.js b/plugins/timeout/src/timeout.js similarity index 100% rename from plugins/src/timeout.js rename to plugins/timeout/src/timeout.js diff --git a/plugins/update/.babelrc b/plugins/update/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/update/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/update/package.json b/plugins/update/package.json new file mode 100644 index 000000000..e358a8f23 --- /dev/null +++ b/plugins/update/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-update", + "version": "1.0.0", + "description": "Plugin for the 'update' term in the Horizon Collections API.", + "main": "dist/update.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/update.js b/plugins/update/src/update.js similarity index 95% rename from plugins/src/update.js rename to plugins/update/src/update.js index 219c99adb..bac8896af 100644 --- a/plugins/src/update.js +++ b/plugins/update/src/update.js @@ -1,9 +1,7 @@ 'use strict'; -const {reqlOptions, versionField: hz_v} = require('./common/utils'); -const writes = require('./common/writes'); - const {r} = require('@horizon/server'); +const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); function update(server) { return (request, response, next) => { diff --git a/plugins/upsert/.babelrc b/plugins/upsert/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/upsert/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/upsert/package.json b/plugins/upsert/package.json new file mode 100644 index 000000000..42405abf4 --- /dev/null +++ b/plugins/upsert/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-upsert", + "version": "1.0.0", + "description": "Plugin for the 'upsert' term in the Horizon Collections API.", + "main": "dist/upsert.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/upsert.js b/plugins/upsert/src/upsert.js similarity index 96% rename from plugins/src/upsert.js rename to plugins/upsert/src/upsert.js index e407a4187..b431b9fe2 100644 --- a/plugins/src/upsert.js +++ b/plugins/upsert/src/upsert.js @@ -1,9 +1,7 @@ 'use strict'; -const {reqlOptions, versionField: hz_v} = require('./common/utils'); -const writes = require('./common/writes'); - const {r} = require('@horizon/server'); +const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); function upsert(server) { return (request, response, next) => { diff --git a/plugins/utils/.babelrc b/plugins/utils/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/utils/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/package.json b/plugins/utils/package.json similarity index 84% rename from plugins/package.json rename to plugins/utils/package.json index f783e5250..75272595a 100644 --- a/plugins/package.json +++ b/plugins/utils/package.json @@ -1,8 +1,8 @@ { - "name": "@horizon/default-plugins", + "name": "@horizon/plugin-utils", "version": "1.0.0", - "description": "Plugins for the default Horizon Collections API.", - "main": "dist/index.js", + "description": "Utilities for the default Horizon plugins.", + "main": "dist/utils.js", "scripts": { "lint": "eslint src", "test": "mocha dist/test", diff --git a/plugins/src/common/reads.js b/plugins/utils/src/reads.js similarity index 100% rename from plugins/src/common/reads.js rename to plugins/utils/src/reads.js diff --git a/plugins/src/common/utils.js b/plugins/utils/src/utils.js similarity index 78% rename from plugins/src/common/utils.js rename to plugins/utils/src/utils.js index 15d74d752..230017bf7 100644 --- a/plugins/src/common/utils.js +++ b/plugins/utils/src/utils.js @@ -1,6 +1,8 @@ 'use strict'; -const MIN_VERSION = [2, 3, 1]; +const {r} = require('@horizon/server'); + +const minRethinkdbVersion = [2, 3, 1]; // Recursive version compare, could be flatter but opted for instant return if // comparison is greater rather than continuing to compare to end. @@ -24,13 +26,13 @@ function rethinkdbVersionCheck(version_string) { // Convert strings to ints and remove first match const versions = matches.slice(1).map((val) => parseInt(val)); - if (!versionCompare(versions, MIN_VERSION)) { + if (!versionCompare(versions, minRethinkdbVersion)) { throw new Error(`RethinkDB (${versions.join('.')}) is below required version ` + - `(${MIN_VERSION.join('.')}) for use with Horizon.`); + `(${minRethinkdbVersion.join('.')}) for use with Horizon.`); } } else { throw new Error('Unable to determine RethinkDB version, check ' + - `RethinkDB is >= ${MIN_VERSION.join('.')}.`); + `RethinkDB is >= ${minRethinkdbVersion.join('.')}.`); } } @@ -54,9 +56,14 @@ const reqlOptions = { const versionField = '$hz_v$'; module.exports = { + metadataVersion, + createCollection, + initializeMetadata, rethinkdbVersionCheck, remakeError, isObject, reqlOptions, versionField, + reads: require('./reads'), + writes: require('./writes'), }; diff --git a/plugins/src/common/writes.js b/plugins/utils/src/writes.js similarity index 100% rename from plugins/src/common/writes.js rename to plugins/utils/src/writes.js diff --git a/plugins/watch/.babelrc b/plugins/watch/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugins/watch/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugins/watch/package.json b/plugins/watch/package.json new file mode 100644 index 000000000..bbdad4a81 --- /dev/null +++ b/plugins/watch/package.json @@ -0,0 +1,38 @@ +{ + "name": "@horizon/plugins-watch", + "version": "1.0.0", + "description": "Plugin for the 'watch' term in the Horizon Collections API.", + "main": "dist/watch.js", + "scripts": { + "lint": "eslint src", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + "@horizon/plugin-utils": "1.0.0" + }, + "peerDependencies": { + "@horizon/plugin-router": "1.x", + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^2.3.0", + "istanbul": "^0.4.3", + "mocha": "^2.3.3", + "babel-cli": "^6.10.1", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugins/src/watch.js b/plugins/watch/src/watch.js similarity index 92% rename from plugins/src/watch.js rename to plugins/watch/src/watch.js index f07cc0e3c..2aeb42417 100644 --- a/plugins/src/watch.js +++ b/plugins/watch/src/watch.js @@ -1,7 +1,6 @@ 'use strict'; -const {reqlOptions} = require('./common/utils'); -const {makeReadReql} = require('./common/reads'); +const {reqlOptions, reads} = require('@horizon/plugin-utils'); function watch(server) { return (req, res, next) => { @@ -14,7 +13,7 @@ function watch(server) { } else if (!permissions) { next(new Error('"watch" requires permissions to run')); } else { - makeReadReql(req).then((reql) => + reads.makeReadReql(req).then((reql) => reql.changes({ include_initial: true, include_states: true, From 9cb6e71522fcdf2c5dde1948450201e139a3a8e4 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 06:20:21 -0700 Subject: [PATCH 54/86] reorganizing some plugin stuff --- plugins/collection/src/collection.js | 25 ++- plugins/collection/src/collection/index.js | 199 ------------------ plugins/collection/src/indexes.js | 100 +++++++++ plugins/collection/src/queries.js | 43 ++++ .../src/{collection => types}/collection.js | 6 +- plugins/collection/src/types/index.js | 109 ++++++++++ .../src/{collection => types}/metadata.js | 63 ++---- .../src/{collection => types}/table.js | 18 +- plugins/utils/src/utils.js | 5 - 9 files changed, 297 insertions(+), 271 deletions(-) delete mode 100644 plugins/collection/src/collection/index.js create mode 100644 plugins/collection/src/indexes.js create mode 100644 plugins/collection/src/queries.js rename plugins/collection/src/{collection => types}/collection.js (97%) create mode 100644 plugins/collection/src/types/index.js rename plugins/collection/src/{collection => types}/metadata.js (87%) rename plugins/collection/src/{collection => types}/table.js (87%) diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 8d179cecb..e0f446d5e 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -1,6 +1,8 @@ 'use strict'; -const {ReliableMetadata} = require('./collection/metadata.js'); +const ReliableMetadata = require('./metadata.js'); +const queries = require('./queries'); +const indexes = require('./indexes'); function collection(metadata) { return (req, res, next) => { @@ -20,19 +22,17 @@ function collection(metadata) { } module.exports = (options) => { - const metadataSymbol = Symbol(); + const metadata = Symbol('hz_collection_metadata'); return { name: 'hz_collection', activate: (ctx, onReady, onUnready) => { - const metadata = new ReliableMetadata( + ctx[metadata] = new ReliableMetadata( ctx, options.auto_create_collection, options.auto_create_index); - ctx[metadataSymbol] = metadata; - - metadata.subscribe({onReady: () => { + ctx[metadata].subscribe({onReady: () => { console.log('metadata ready'); onReady(); }, onUnready}); @@ -41,16 +41,21 @@ module.exports = (options) => { methods: { collection: { type: 'option', - handler: collection(metadata), + handler: collection(ctx[metadata]), }, }, }; }, deactivate: (ctx) => { - const metadata = ctx[metadataSymbol]; - if (metadata) { - metadata.close(); + if (ctx[metadata]) { + ctx[metadata].close(); } }, }; }; + +module.exports.createCollection = queries.createCollection; +module.exports.initializeMetadata = queries.initializeMetadata; +module.exports.indexNameToInfo = indexes.indexNameToInfo; +module.exports.indexInfoToName = indexes.indexInfoToName; +module.exports.indexInfoToReql = indexes.indexInfoToReql; diff --git a/plugins/collection/src/collection/index.js b/plugins/collection/src/collection/index.js deleted file mode 100644 index 4a4024420..000000000 --- a/plugins/collection/src/collection/index.js +++ /dev/null @@ -1,199 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -const {logger} = require('@horizon/server'); - -// Index names are of the format "hz_[_]" where may be -// omitted or "multi_" or "geo" (at the moment). is a JSON array -// specifying which fields are indexed in which order. The value at each index -// in the array is either a nested array (for indexing nested fields) or a string -// for a root-level field name. -// -// Example: -// Fields indexed: foo.bar, baz -// Index name: hz_[["foo","bar"],"baz"] -const primary_index_name = 'id'; - -const name_to_info = (name) => { - if (name === primary_index_name) { - return {geo: false, multi: false, fields: ['id']}; - } - - const re = /^hz_(?:(geo)_)?(?:multi_([0-9])+_)?\[/; - - const matches = name.match(re); - assert(matches !== null, `Unexpected index name (invalid format): "${name}"`); - - const json_offset = matches[0].length - 1; - - const info = { - name, - geo: Boolean(matches[1]), - multi: isNaN(matches[2]) ? false : Number(matches[2]), - }; - - // Parse remainder as JSON - try { - info.fields = JSON.parse(name.slice(json_offset)); - } catch (err) { - assert(false, `Unexpected index name (invalid JSON): "${name}"`); - } - - // Sanity check fields - const validate_field = (f) => { - assert(Array.isArray(f), `Unexpected index name (invalid field): "${name}"`); - f.forEach((s) => assert(typeof s === 'string', - `Unexpected index name (invalid field): "${name}"`)); - }; - - assert(Array.isArray(info.fields), - `Unexpected index name (fields are not an array): "${name}"`); - assert((info.multi === false) || (info.multi < info.fields.length), - `Unexpected index name (multi index out of bounds): "${name}"`); - info.fields.forEach(validate_field); - return info; -}; - -const info_to_name = (info) => { - let res = 'hz_'; - if (info.geo) { - res += 'geo_'; - } - if (info.multi !== false) { - res += 'multi_' + info.multi + '_'; - } - res += JSON.stringify(info.fields); - return res; -}; - -const info_to_reql = (info) => { - if (info.geo && (info.multi !== false)) { - throw new Error('multi and geo cannot be specified on the same index'); - } - - if (info.multi !== false) { - const multi_field = info.fields[info.multi]; - return (row) => - row(multi_field).map((value) => info.fields.map((f, i) => { - if (i === info.multi) { - return value; - } else { - let res = row; - f.forEach((field_name) => { res = res(field_name); }); - return res; - } - })); - } else { - return (row) => - info.fields.map((f) => { - let res = row; - f.forEach((field_name) => { res = res(field_name); }); - return res; - }); - } -}; - -const compare_fields = (a, b) => { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; ++i) { - if (a[i] !== b[i]) { - return false; - } - } - return true; -}; - -class Index { - constructor(name, table, conn) { - logger.debug(`${table} index registered: ${name}`); - const info = name_to_info(name); - this.name = name; - this.geo = info.geo; // true or false - this.multi = info.multi; // false or the offset of the multi field - this.fields = info.fields; // array of fields or nested field paths - - this._waiters = []; - this._result = null; - - if (this.geo) { - logger.warn(`Unsupported index (geo): ${this.name}`); - } else if (this.multi !== false) { - logger.warn(`Unsupported index (multi): ${this.name}`); - } - - if (name !== primary_index_name) { - table.indexWait(name).run(conn).then(() => { - logger.debug(`${table} index ready: ${name}`); - this._result = true; - this._waiters.forEach((w) => w(this)); - this._waiters = []; - }).catch((err) => { - this._result = err; - this._waiters.forEach((w) => w(err)); - this._waiters = []; - }); - } else { - logger.debug(`${table} index ready: ${name}`); - this._result = true; - } - } - - close() { - this._waiters.forEach((w) => w(new Error('index deleted'))); - this._waiters = []; - } - - ready() { - return this._result === true; - } - - on_ready(done) { - if (this._result === true) { - done(); - } else if (this._result) { - done(this); - } else { - this._waiters.push(done); - } - } - - // `fuzzy_fields` may be in any order at the beginning of the index. - // These must be immediately followed by `ordered_fields` in the exact - // order given. There may be no other fields present in the index - // (because the absence of a field would mean that row is not indexed). - // `fuzzy_fields` may overlap with `ordered_fields`. - is_match(fuzzy_fields, ordered_fields) { - // TODO: multi index matching - if (this.geo || this.multi !== false) { - return false; - } - - if (this.fields.length > fuzzy_fields.length + ordered_fields.length || - this.fields.length < fuzzy_fields.length || - this.fields.length < ordered_fields.length) { - return false; - } - - for (let i = 0; i < fuzzy_fields.length; ++i) { - let found = false; - for (let j = 0; j < fuzzy_fields.length && !found; ++j) { - found = compare_fields(fuzzy_fields[i], this.fields[j]); - } - if (!found) { return false; } - } - - for (let i = 0; i < ordered_fields.length; ++i) { - const pos = this.fields.length - ordered_fields.length + i; - if (pos < 0 || !compare_fields(ordered_fields[i], this.fields[pos])) { - return false; - } - } - - return true; - } -} - -module.exports = {Index, primary_index_name, name_to_info, info_to_name, info_to_reql}; diff --git a/plugins/collection/src/indexes.js b/plugins/collection/src/indexes.js new file mode 100644 index 000000000..b1f028aee --- /dev/null +++ b/plugins/collection/src/indexes.js @@ -0,0 +1,100 @@ +'use strict'; + +const assert = require('assert'); + +const primaryIndexName = 'id'; + +// Index names are of the format "hz_[_]" where may be +// omitted or "multi_" or "geo" (at the moment). is a JSON array +// specifying which fields are indexed in which order. The value at each index +// in the array is either a nested array (for indexing nested fields) or a string +// for a root-level field name. +// +// Example: +// Fields indexed: foo.bar, baz +// Index name: hz_[["foo","bar"],"baz"] +function indexNameToInfo(name) { + if (name === primaryIndexName) { + return {geo: false, multi: false, fields: ['id']}; + } + + const re = /^hz_(?:(geo)_)?(?:multi_([0-9])+_)?\[/; + + const matches = name.match(re); + assert(matches !== null, `Unexpected index name (invalid format): "${name}"`); + + const json_offset = matches[0].length - 1; + + const info = { + name, + geo: Boolean(matches[1]), + multi: isNaN(matches[2]) ? false : Number(matches[2]), + }; + + // Parse remainder as JSON + try { + info.fields = JSON.parse(name.slice(json_offset)); + } catch (err) { + assert(false, `Unexpected index name (invalid JSON): "${name}"`); + } + + // Sanity check fields + const validate_field = (f) => { + assert(Array.isArray(f), `Unexpected index name (invalid field): "${name}"`); + f.forEach((s) => assert(typeof s === 'string', + `Unexpected index name (invalid field): "${name}"`)); + }; + + assert(Array.isArray(info.fields), + `Unexpected index name (fields are not an array): "${name}"`); + assert((info.multi === false) || (info.multi < info.fields.length), + `Unexpected index name (multi index out of bounds): "${name}"`); + info.fields.forEach(validate_field); + return info; +} + +function indexInfoToName(info) { + let res = 'hz_'; + if (info.geo) { + res += 'geo_'; + } + if (info.multi !== false) { + res += 'multi_' + info.multi + '_'; + } + res += JSON.stringify(info.fields); + return res; +} + +function indexInfoToReql(info) { + if (info.geo && (info.multi !== false)) { + throw new Error('multi and geo cannot be specified on the same index'); + } + + if (info.multi !== false) { + const multi_field = info.fields[info.multi]; + return (row) => + row(multi_field).map((value) => info.fields.map((f, i) => { + if (i === info.multi) { + return value; + } else { + let res = row; + f.forEach((field_name) => { res = res(field_name); }); + return res; + } + })); + } else { + return (row) => + info.fields.map((f) => { + let res = row; + f.forEach((field_name) => { res = res(field_name); }); + return res; + }); + } +} + +module.exports = { + indexInfoToName, + indexInfoToReql, + indexNameToInfo, + primaryIndexName, +}; diff --git a/plugins/collection/src/queries.js b/plugins/collection/src/queries.js new file mode 100644 index 000000000..5a77205f3 --- /dev/null +++ b/plugins/collection/src/queries.js @@ -0,0 +1,43 @@ +'use strict'; + +const {r} = require('@horizon/server'); + +const metadataVersion = [2, 0, 0]; + +function createCollection(db, name, conn) { + return r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => + r.branch( + res('errors').ne(0), + r.error(res('first_error')), + res('inserted').eq(1), + r.db(db).tableCreate(name), + res + ) + ).run(conn); +} + +function initializeMetadata(db, conn) { + return r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) + .then(() => + Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => + r.branch(r.db(db).tableList().contains(table), + { }, + r.db(db).tableCreate(table)) + .run(conn)))) + .then(() => + r.db(db).table('hz_collections').wait({timeout: 30}).run(conn)) + .then(() => + Promise.all([ + r.db(db).tableList().contains('users').not().run(conn).then(() => + createCollection(r, db, 'users', conn)), + r.db(db).table('hz_collections') + .insert({id: 'hz_metadata', version: metadataVersion}) + .run(conn), + ]) + ); +} + +module.exports = { + createCollection, + initializeMetadata, +}; diff --git a/plugins/collection/src/collection/collection.js b/plugins/collection/src/types/collection.js similarity index 97% rename from plugins/collection/src/collection/collection.js rename to plugins/collection/src/types/collection.js index 03ec02827..ea676328e 100644 --- a/plugins/collection/src/collection/collection.js +++ b/plugins/collection/src/types/collection.js @@ -1,8 +1,8 @@ 'use strict'; -const {r} = require('@horizon/server'); +const Table = require('./table'); -const Table = require('./table').Table; +const {r} = require('@horizon/server'); class Collection { constructor(db, name, reliableConn) { @@ -97,4 +97,4 @@ class Collection { } } -module.exports = {Collection}; +module.exports = Collection; diff --git a/plugins/collection/src/types/index.js b/plugins/collection/src/types/index.js new file mode 100644 index 000000000..754c86d25 --- /dev/null +++ b/plugins/collection/src/types/index.js @@ -0,0 +1,109 @@ +'use strict'; + +const {indexNameToInfo, primaryIndexName} = require('../indexes'); + +const {logger} = require('@horizon/server'); + +const compare_fields = (a, b) => { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +}; + +class Index { + constructor(name, table, conn) { + logger.debug(`${table} index registered: ${name}`); + const info = indexNameToInfo(name); + this.name = name; + this.geo = info.geo; // true or false + this.multi = info.multi; // false or the offset of the multi field + this.fields = info.fields; // array of fields or nested field paths + + this._waiters = []; + this._result = null; + + if (this.geo) { + logger.warn(`Unsupported index (geo): ${this.name}`); + } else if (this.multi !== false) { + logger.warn(`Unsupported index (multi): ${this.name}`); + } + + if (name !== primaryIndexName) { + table.indexWait(name).run(conn).then(() => { + logger.debug(`${table} index ready: ${name}`); + this._result = true; + this._waiters.forEach((w) => w(this)); + this._waiters = []; + }).catch((err) => { + this._result = err; + this._waiters.forEach((w) => w(err)); + this._waiters = []; + }); + } else { + logger.debug(`${table} index ready: ${name}`); + this._result = true; + } + } + + close() { + this._waiters.forEach((w) => w(new Error('index deleted'))); + this._waiters = []; + } + + ready() { + return this._result === true; + } + + on_ready(done) { + if (this._result === true) { + done(); + } else if (this._result) { + done(this); + } else { + this._waiters.push(done); + } + } + + // `fuzzy_fields` may be in any order at the beginning of the index. + // These must be immediately followed by `ordered_fields` in the exact + // order given. There may be no other fields present in the index + // (because the absence of a field would mean that row is not indexed). + // `fuzzy_fields` may overlap with `ordered_fields`. + is_match(fuzzy_fields, ordered_fields) { + // TODO: multi index matching + if (this.geo || this.multi !== false) { + return false; + } + + if (this.fields.length > fuzzy_fields.length + ordered_fields.length || + this.fields.length < fuzzy_fields.length || + this.fields.length < ordered_fields.length) { + return false; + } + + for (let i = 0; i < fuzzy_fields.length; ++i) { + let found = false; + for (let j = 0; j < fuzzy_fields.length && !found; ++j) { + found = compare_fields(fuzzy_fields[i], this.fields[j]); + } + if (!found) { return false; } + } + + for (let i = 0; i < ordered_fields.length; ++i) { + const pos = this.fields.length - ordered_fields.length + i; + if (pos < 0 || !compare_fields(ordered_fields[i], this.fields[pos])) { + return false; + } + } + + return true; + } +} + +module.exports = Index; diff --git a/plugins/collection/src/collection/metadata.js b/plugins/collection/src/types/metadata.js similarity index 87% rename from plugins/collection/src/collection/metadata.js rename to plugins/collection/src/types/metadata.js index a311c692a..56618cf1e 100644 --- a/plugins/collection/src/collection/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -1,46 +1,19 @@ 'use strict'; -const Collection = require('./collection').Collection; +const queries = require('../queries'); +const Collection = require('./types/collection'); const assert = require('assert'); -const {r, logger, Reliable, ReliableUnion} = require('@horizon/server'); -const {rethinkdbVersionCheck} = require('@horizon/plugin-utils'); - -const metadataVersion = [2, 0, 0]; - -function createCollection(db, name, conn) { - return r.db(db).table('hz_collections').get(name).replace({id: name}).do((res) => - r.branch( - res('errors').ne(0), - r.error(res('first_error')), - res('inserted').eq(1), - r.db(db).tableCreate(name), - res - ) - ).run(conn); -} +const { + r, + logger, + Reliable, + ReliableUnion, + ReliableChangefeed, +} = require('@horizon/server'); -function initializeMetadata(db, conn) { - return r.branch(r.dbList().contains(db), null, r.dbCreate(db)).run(conn) - .then(() => - Promise.all(['hz_collections', 'hz_users_auth', 'hz_groups'].map((table) => - r.branch(r.db(db).tableList().contains(table), - { }, - r.db(db).tableCreate(table)) - .run(conn)))) - .then(() => - r.db(db).table('hz_collections').wait({timeout: 30}).run(conn)) - .then(() => - Promise.all([ - r.db(db).tableList().contains('users').not().run(conn).then(() => - createCollection(r, db, 'users', conn)), - r.db(db).table('hz_collections') - .insert({id: 'hz_metadata', version: metadataVersion}) - .run(conn), - ]) - ); -} +const {rethinkdbVersionCheck} = require('@horizon/plugin-utils'); class StaleAttemptError extends Error { } @@ -91,7 +64,7 @@ class ReliableInit extends Reliable { this.check_attempt(attempt); logger.debug('checking for internal tables'); if (this._auto_create_collection) { - return initializeMetadata(this._db, conn); + return queries.initializeMetadata(this._db, conn); } else { return r.dbList().contains(this._db).run(conn).then((has_db) => { if (!has_db) { @@ -180,11 +153,12 @@ class ReliableMetadata extends Reliable { }); // RSI: stop these from running until after ReliableInit? - this._collection_changefeed = server.makeReliableChangefeed( + this._collection_changefeed = new ReliableChangefeed( r.db(this._db) .table('hz_collections') .filter((row) => row('id').match('^hzp?_').not()) .changes({squash: false, includeInitial: true, includeTypes: true}), + this._reliable_conn, { onChange: (change) => { switch (change.type) { @@ -223,7 +197,7 @@ class ReliableMetadata extends Reliable { }, }); - this._index_changefeed = server.makeReliableChangefeed( + this._index_changefeed = new ReliableChangefeed( r.db('rethinkdb') .table('table_config') .filter((row) => r.and(row('db').eq(this._db), @@ -234,6 +208,7 @@ class ReliableMetadata extends Reliable { indexes: row('indexes').filter((idx) => idx.match('^hz_')), })) .changes({squash: true, includeInitial: true, includeTypes: true}), + this._reliable_conn, { onChange: (change) => { if (!this._connection) { return; } @@ -348,7 +323,7 @@ class ReliableMetadata extends Reliable { collection = new Collection(this._db, name, this._reliable_conn); this._collections.set(name, collection); - return createCollection(this._db, name, this._reliable_conn.connection()); + return queries.createCollection(this._db, name, this._reliable_conn.connection()); }).then((res) => { assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); logger.warn(`Collection created: "${name}"`); @@ -370,8 +345,4 @@ class ReliableMetadata extends Reliable { } } -module.exports = { - createCollection, - initializeMetadata, - ReliableMetadata, -}; +module.exports = ReliableMetadata; diff --git a/plugins/collection/src/collection/table.js b/plugins/collection/src/types/table.js similarity index 87% rename from plugins/collection/src/collection/table.js rename to plugins/collection/src/types/table.js index 15530cbd1..08e64e86e 100644 --- a/plugins/collection/src/collection/table.js +++ b/plugins/collection/src/types/table.js @@ -1,6 +1,8 @@ 'use strict'; -const index = require('./index'); +const Index = require('./index'); +const {primaryIndexName, indexInfoToReql, indexInfoToName} = require('../indexes'); + const assert = require('assert'); const {r, logger} = require('@horizon/server'); @@ -53,13 +55,13 @@ class Table { logger.debug(`${this.table} indexes changed, reevaluating`); // Initialize the primary index, which won't show up in the changefeed - indexes.push(index.primary_index_name); + indexes.push(primaryIndexName); const new_index_map = new Map(); indexes.forEach((name) => { try { const old_index = this.indexes.get(name); - const new_index = new index.Index(name, this.table, conn); + const new_index = new Index(name, this.table, conn); if (old_index) { // Steal any waiters from the old index new_index._waiters = old_index._waiters; @@ -79,19 +81,19 @@ class Table { // TODO: support geo and multi indexes create_index(fields, conn, done) { const info = {geo: false, multi: false, fields}; - const index_name = index.info_to_name(info); + const index_name = indexInfoToName(info); assert(!this.indexes.get(index_name), 'index already exists'); const success = () => { // Create the Index object now so we don't try to create it again before the // feed notifies us of the index creation - const new_index = new index.Index(index_name, this.table, conn); + const new_index = new Index(index_name, this.table, conn); // TODO: shouldn't this be done before we go async? this.indexes.set(index_name, new_index); return new_index.on_ready(done); }; - this.table.indexCreate(index_name, index.info_to_reql(info), + this.table.indexCreate(index_name, indexInfoToReql(info), {geo: info.geo, multi: (info.multi !== false)}) .run(conn) .then(success) @@ -109,7 +111,7 @@ class Table { // fuzzy_fields and ordered_fields should both be arrays get_matching_index(fuzzy_fields, ordered_fields) { if (fuzzy_fields.length === 0 && ordered_fields.length === 0) { - return this.indexes.get(index.primary_index_name); + return this.indexes.get(primaryIndexName); } let match; @@ -127,4 +129,4 @@ class Table { } } -module.exports = {Table}; +module.exports = Table; diff --git a/plugins/utils/src/utils.js b/plugins/utils/src/utils.js index 230017bf7..08e7278a8 100644 --- a/plugins/utils/src/utils.js +++ b/plugins/utils/src/utils.js @@ -1,7 +1,5 @@ 'use strict'; -const {r} = require('@horizon/server'); - const minRethinkdbVersion = [2, 3, 1]; // Recursive version compare, could be flatter but opted for instant return if @@ -56,9 +54,6 @@ const reqlOptions = { const versionField = '$hz_v$'; module.exports = { - metadataVersion, - createCollection, - initializeMetadata, rethinkdbVersionCheck, remakeError, isObject, From 9cf5acd52af7081cff3cce3e7e7d7fb79100b669 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 07:29:57 -0700 Subject: [PATCH 55/86] added module for default plugins --- plugins/collection/src/collection.js | 4 +- plugins/defaults/package.json | 18 +++++ plugins/defaults/src/defaults.js | 117 ++++++++++++++++++++++----- plugins/timeout/src/timeout.js | 4 +- 4 files changed, 117 insertions(+), 26 deletions(-) diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index e0f446d5e..a550b60cb 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -29,8 +29,8 @@ module.exports = (options) => { activate: (ctx, onReady, onUnready) => { ctx[metadata] = new ReliableMetadata( ctx, - options.auto_create_collection, - options.auto_create_index); + Boolean(options.auto_create_collection), + Boolean(options.auto_create_index)); ctx[metadata].subscribe({onReady: () => { console.log('metadata ready'); diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json index f3c263a86..6ad048065 100644 --- a/plugins/defaults/package.json +++ b/plugins/defaults/package.json @@ -22,6 +22,24 @@ "node": ">=4.0.0" }, "dependencies": { + "@horizon/plugin-above": "1.0.0", + "@horizon/plugin-below": "1.0.0", + "@horizon/plugin-collection": "1.0.0", + "@horizon/plugin-fetch": "1.0.0", + "@horizon/plugin-find": "1.0.0", + "@horizon/plugin-findAll": "1.0.0", + "@horizon/plugin-insert": "1.0.0", + "@horizon/plugin-limit": "1.0.0", + "@horizon/plugin-order": "1.0.0", + "@horizon/plugin-permissions": "1.0.0", + "@horizon/plugin-permit-all": "1.0.0", + "@horizon/plugin-remove": "1.0.0", + "@horizon/plugin-replace": "1.0.0", + "@horizon/plugin-store": "1.0.0", + "@horizon/plugin-timeout": "1.0.0", + "@horizon/plugin-update": "1.0.0", + "@horizon/plugin-upsert": "1.0.0", + "@horizon/plugin-watch": "1.0.0" }, "peerDependencies": { "@horizon/plugin-router": "1.x", diff --git a/plugins/defaults/src/defaults.js b/plugins/defaults/src/defaults.js index be22679ae..da31c012f 100644 --- a/plugins/defaults/src/defaults.js +++ b/plugins/defaults/src/defaults.js @@ -1,25 +1,98 @@ 'use strict'; -module.exports = { - // Collections API - above: require('./above'), - below: require('./below'), - collection: require('./collection'), - insert: require('./insert'), - fetch: require('./fetch'), - find: require('./find'), - findAll: require('./findAll'), - limit: require('./limit'), - order: require('./order'), - remove: require('./remove'), - replace: require('./replace'), - store: require('./store'), - timeout: require('./timeout'), - update: require('./update'), - upsert: require('./upsert'), - watch: require('./watch'), - - // Permissions API - permissions: require('./permissions'), - permit_all: require('./permit_all'), +// Collections API +const defaultMethods = { + above: require('@horizon/plugins-above'), + below: require('@horizon/plugins-below'), + collection: require('@horizon/plugins-collection'), + insert: require('@horizon/plugins-insert'), + fetch: require('@horizon/plugins-fetch'), + find: require('@horizon/plugins-find'), + findAll: require('@horizon/plugins-findAll'), + limit: require('@horizon/plugins-limit'), + order: require('@horizon/plugins-order'), + remove: require('@horizon/plugins-remove'), + replace: require('@horizon/plugins-replace'), + store: require('@horizon/plugins-store'), + timeout: require('@horizon/plugins-timeout'), + update: require('@horizon/plugins-update'), + upsert: require('@horizon/plugins-upsert'), + watch: require('@horizon/plugins-watch'), }; + +// Permissions API +const defaultPermissions = { + permissions: require('@horizon/plugins-permissions'), + permit_all: require('@horizon/plugins-permit-all'), +}; + +// Combines some subset of the default plugins into a single plugin for ease-of-use +// `raw_config` can be omitted or an object with any or all of these properties: +// `methods`: an array of default methods to include, defaults to all of them +// `permissions`: +// false: no permissions plugin will be loaded (the collections API won't work +// unless some other plugin provides the 'hz_permissions' prereq) +// 'permissions': the standard permissions plugin will be loaded (default) +// 'permit-all': a dummy permissions plugin will be loaded that allows all requests +module.exports = (raw_config) => { + const config = raw_config || {}; + const subplugins = (config.methods || Object.keys(defaultMethods)).map((name) => { + const plugin = defaultMethods[name]; + if (!plugin) { + throw new Error(`Method "${name}" is not provided by a default Horizon plugin.`); + } + return plugin(config); + }); + + if (config.permissions === undefined) { + // Use the secure thing by default + subplugins.push(defaultPermissions.permissions(config)); + } else if (config.permissions !== false) { + const plugin = defaultPermissions[config.permissions]; + if (!plugin) { + throw new Error(`Unrecognized permissions plugin name "${config.permissions}", ` + + 'expected "permissions" or "permit-all".'); + } + subplugins.push(plugin(config)); + } + + return { + name: 'hz_defaults', + activate: (ctx, onReady, onUnready) => { + // Some subplugins may need to notify about readiness + const readyPlugins = new Map(); + function ready(name) { + readyPlugins.set(name); + if (readyPlugins.size === subplugins.length) { + onReady(); + } + } + function unready(name) { + if (readyPlugins.size === subplugins.length) { + onUnready(); + } + readyPlugins.delete(name); + } + + const promises = subplugins.map((plugin) => { + const promise = Promise.resolve().then(() => + plugin.activate(ctx, () => ready(plugin.name), () => unready(plugin.name)) + ); + if (plugin.activate.length < 2) { + ready(plugin.name); + } + return promise; + }); + + return Promise.all(promises).then((results) => ({ + methods: Object.assign({}, ...results.map((i) => i.methods)), + })); + }, + deactivate: (ctx) => + Promise.all(subplugins.map((p) => + Promise.resolve().then(() => p.deactivate && p.deactivate(ctx)))), + }; +}; + +module.exports.methods = defaultMethods; +module.exports.permissions = defaultPermissions; diff --git a/plugins/timeout/src/timeout.js b/plugins/timeout/src/timeout.js index 686a42d41..7a4275a5d 100644 --- a/plugins/timeout/src/timeout.js +++ b/plugins/timeout/src/timeout.js @@ -15,11 +15,11 @@ function timeout(req, res, next) { module.exports = () => ({ name: 'hz_timeout', activate: () => ({ - methods: ({ + methods: { timeout: { type: 'option', handler: timeout, }, - }), + }, }), }); From dead3b0f173d85f8d43c487dabc661fe5033830b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 09:50:38 -0700 Subject: [PATCH 56/86] working on getting dev environment easy to set up --- Dockerfile | 2 +- cli/package.json | 6 +-- cli/src/utils/start_rdb_server.js | 2 +- client/README.md | 2 +- client/package.json | 6 +-- plugin-router/package.json | 6 +-- plugins/above/package.json | 6 +-- plugins/below/package.json | 6 +-- plugins/collection/package.json | 8 ++-- plugins/collection/src/collection.js | 2 +- plugins/collection/src/types/metadata.js | 2 +- plugins/defaults/package.json | 6 +-- plugins/defaults/src/defaults.js | 36 ++++++++--------- plugins/fetch/package.json | 8 ++-- plugins/find/package.json | 8 ++-- plugins/findAll/package.json | 8 ++-- plugins/insert/package.json | 8 ++-- plugins/limit/package.json | 8 ++-- plugins/order/package.json | 8 ++-- plugins/permissions/package.json | 8 ++-- plugins/permit-all/package.json | 8 ++-- plugins/remove/package.json | 8 ++-- plugins/replace/package.json | 8 ++-- plugins/store/package.json | 8 ++-- plugins/timeout/package.json | 8 ++-- plugins/update/package.json | 8 ++-- plugins/upsert/package.json | 8 ++-- plugins/utils/package.json | 6 +-- plugins/watch/package.json | 8 ++-- server/package.json | 11 +++--- setupDev.sh | 50 ++++++++++++++++++++++++ test/package.json | 10 ++--- test/serve.js | 39 ++++++------------ test/setupDev.sh | 47 ---------------------- 34 files changed, 179 insertions(+), 194 deletions(-) create mode 100755 setupDev.sh delete mode 100755 test/setupDev.sh diff --git a/Dockerfile b/Dockerfile index df0c06170..16449c6f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apt update && apt install -y git COPY . /usr/horizon/ WORKDIR /usr/horizon -RUN cd test; ./setupDev.sh +RUN ./setupDev.sh EXPOSE 8181 diff --git a/cli/package.json b/cli/package.json index c80c580ae..91d0eccef 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "horizon", - "version": "2.0.0-beta-5", + "version": "3.0.0-alpha-0", "description": "An open-source developer platform for building realtime, scalable web apps.", "main": "src/main.js", "repository": { @@ -37,9 +37,9 @@ }, "devDependencies": { "chai": "^3.5.0", - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "2.4.5", + "mocha": "^2.5.3", "mock-fs": "3.8.0", "sinon": "1.17.3", "toml": "^2.3.0" diff --git a/cli/src/utils/start_rdb_server.js b/cli/src/utils/start_rdb_server.js index c9e02ba0c..e665d186c 100644 --- a/cli/src/utils/start_rdb_server.js +++ b/cli/src/utils/start_rdb_server.js @@ -2,7 +2,7 @@ const each_line_in_pipe = require('./each_line_in_pipe'); const horizon_server = require('@horizon/server'); -const versionCheck = require('@horizon/plugins/common/utils').rethinkdbVersionCheck; +const versionCheck = require('@horizon/plugin-utils').rethinkdbVersionCheck; const execSync = require('child_process').execSync; const spawn = require('child_process').spawn; diff --git a/client/README.md b/client/README.md index 625fbc2be..11e00e257 100644 --- a/client/README.md +++ b/client/README.md @@ -26,7 +26,7 @@ npm run test | Run tests ## Running tests * `npm test` or open `dist/test.html` in your browser after getting setup and while you also have Horizon server with the `--dev` flag running on `localhost`. -* You can spin up a dev server by cloning the horizon repo and running `node serve.js` in `test` directory in repo root. Then tests can be accessed from . Source maps work properly when served via http, not from file system. You can test the production version via `NODE_ENV=production node serve.js`. You may want to use `test/setupDev.sh` to set the needed local npm links for development. +* You can spin up a dev server by cloning the horizon repo and running `node serve.js` in `test` directory in repo root. Then tests can be accessed from . Source maps work properly when served via http, not from file system. You can test the production version via `NODE_ENV=production node serve.js`. You may want to use `setupDev.sh` to set the needed local npm links for development. ## Docs diff --git a/client/package.json b/client/package.json index df671fef0..67ba665be 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "@horizon/client", - "version": "2.0.0-beta-5", + "version": "3.0.0-alpha-0", "description": "RethinkDB Horizon is an open-source developer platform for building realtime, scalable web apps.", "scripts": { "coverage": "cross-env NODE_ENV=test nyc mocha test/test.js", @@ -33,7 +33,7 @@ "babel-loader": "^6.2.4", "babel-plugin-istanbul": "^1.0.3", "babel-plugin-transform-runtime": "^6.6.0", - "babel-preset-es2015": "^6.6.0", + "babel-preset-es2015": "^6.9.0", "babel-preset-es2015-loose": "^7.0.0", "babel-register": "^6.9.0", "chai": "^3.5.0", @@ -42,7 +42,7 @@ "eslint": "^3.1.0", "exports-loader": "^0.6.3", "imports-loader": "^0.6.5", - "istanbul": "^0.4.2", + "istanbul": "^0.4.3", "lodash.clonedeep": "^4.4.1", "lodash.sortby": "^4.6.1", "mocha": "^2.5.3", diff --git a/plugin-router/package.json b/plugin-router/package.json index 547aff22c..0bb3ae123 100644 --- a/plugin-router/package.json +++ b/plugin-router/package.json @@ -27,10 +27,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/above/package.json b/plugins/above/package.json index 7fcf4c666..f7fd4e442 100644 --- a/plugins/above/package.json +++ b/plugins/above/package.json @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/below/package.json b/plugins/below/package.json index 6b3115da0..34ff8b9b4 100644 --- a/plugins/below/package.json +++ b/plugins/below/package.json @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/collection/package.json b/plugins/collection/package.json index 535789c74..a3a181af4 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-collection", + "name": "@horizon/plugin-collection", "version": "1.0.0", "description": "Plugin for the 'collection' term in the Horizon Collections API.", "main": "dist/collection.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index a550b60cb..2bf0c47c2 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -1,6 +1,6 @@ 'use strict'; -const ReliableMetadata = require('./metadata.js'); +const ReliableMetadata = require('./types/metadata.js'); const queries = require('./queries'); const indexes = require('./indexes'); diff --git a/plugins/collection/src/types/metadata.js b/plugins/collection/src/types/metadata.js index 56618cf1e..24bca9dcb 100644 --- a/plugins/collection/src/types/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -1,7 +1,7 @@ 'use strict'; const queries = require('../queries'); -const Collection = require('./types/collection'); +const Collection = require('./collection'); const assert = require('assert'); diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json index 6ad048065..fde3608e1 100644 --- a/plugins/defaults/package.json +++ b/plugins/defaults/package.json @@ -46,10 +46,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/defaults/src/defaults.js b/plugins/defaults/src/defaults.js index da31c012f..2a899c005 100644 --- a/plugins/defaults/src/defaults.js +++ b/plugins/defaults/src/defaults.js @@ -2,28 +2,28 @@ // Collections API const defaultMethods = { - above: require('@horizon/plugins-above'), - below: require('@horizon/plugins-below'), - collection: require('@horizon/plugins-collection'), - insert: require('@horizon/plugins-insert'), - fetch: require('@horizon/plugins-fetch'), - find: require('@horizon/plugins-find'), - findAll: require('@horizon/plugins-findAll'), - limit: require('@horizon/plugins-limit'), - order: require('@horizon/plugins-order'), - remove: require('@horizon/plugins-remove'), - replace: require('@horizon/plugins-replace'), - store: require('@horizon/plugins-store'), - timeout: require('@horizon/plugins-timeout'), - update: require('@horizon/plugins-update'), - upsert: require('@horizon/plugins-upsert'), - watch: require('@horizon/plugins-watch'), + above: require('@horizon/plugin-above'), + below: require('@horizon/plugin-below'), + collection: require('@horizon/plugin-collection'), + insert: require('@horizon/plugin-insert'), + fetch: require('@horizon/plugin-fetch'), + find: require('@horizon/plugin-find'), + findAll: require('@horizon/plugin-findAll'), + limit: require('@horizon/plugin-limit'), + order: require('@horizon/plugin-order'), + remove: require('@horizon/plugin-remove'), + replace: require('@horizon/plugin-replace'), + store: require('@horizon/plugin-store'), + timeout: require('@horizon/plugin-timeout'), + update: require('@horizon/plugin-update'), + upsert: require('@horizon/plugin-upsert'), + watch: require('@horizon/plugin-watch'), }; // Permissions API const defaultPermissions = { - permissions: require('@horizon/plugins-permissions'), - permit_all: require('@horizon/plugins-permit-all'), + permissions: require('@horizon/plugin-permissions'), + 'permit-all': require('@horizon/plugin-permit-all'), }; // Combines some subset of the default plugins into a single plugin for ease-of-use diff --git a/plugins/fetch/package.json b/plugins/fetch/package.json index 34ed102d3..e174b2c0d 100644 --- a/plugins/fetch/package.json +++ b/plugins/fetch/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-fetch", + "name": "@horizon/plugin-fetch", "version": "1.0.0", "description": "Plugin for the 'fetch' term in the Horizon Collections API.", "main": "dist/fetch.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/find/package.json b/plugins/find/package.json index ed9f753dd..f2198e0ff 100644 --- a/plugins/find/package.json +++ b/plugins/find/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-find", + "name": "@horizon/plugin-find", "version": "1.0.0", "description": "Plugin for the 'find' term in the Horizon Collections API.", "main": "dist/find.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/findAll/package.json b/plugins/findAll/package.json index 41cbf13ad..f0da8be8f 100644 --- a/plugins/findAll/package.json +++ b/plugins/findAll/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-findAll", + "name": "@horizon/plugin-findAll", "version": "1.0.0", "description": "Plugin for the 'findAll' term in the Horizon Collections API.", "main": "dist/findAll.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/insert/package.json b/plugins/insert/package.json index c80c2e0f0..b6a7e55f1 100644 --- a/plugins/insert/package.json +++ b/plugins/insert/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-insert", + "name": "@horizon/plugin-insert", "version": "1.0.0", "description": "Plugin for the 'insert' term in the Horizon Collections API.", "main": "dist/insert.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/limit/package.json b/plugins/limit/package.json index 8b89df64c..809e9e058 100644 --- a/plugins/limit/package.json +++ b/plugins/limit/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-limit", + "name": "@horizon/plugin-limit", "version": "1.0.0", "description": "Plugin for the 'limit' term in the Horizon Collections API.", "main": "dist/limit.js", @@ -28,10 +28,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/order/package.json b/plugins/order/package.json index f4d046802..c44f4e4c1 100644 --- a/plugins/order/package.json +++ b/plugins/order/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-order", + "name": "@horizon/plugin-order", "version": "1.0.0", "description": "Plugin for the 'order' term in the Horizon Collections API.", "main": "dist/order.js", @@ -28,10 +28,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index b6ae84880..cdac6af69 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-permissions", + "name": "@horizon/plugin-permissions", "version": "1.0.0", "description": "Plugin adding user groups and permissions to Horizon.", "main": "dist/permissions.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/permit-all/package.json b/plugins/permit-all/package.json index a306c896a..b9850f479 100644 --- a/plugins/permit-all/package.json +++ b/plugins/permit-all/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-permit-all", + "name": "@horizon/plugin-permit-all", "version": "1.0.0", "description": "Plugin allowing any request to run without permissions in Horizon.", "main": "dist/permit-all.js", @@ -28,10 +28,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/remove/package.json b/plugins/remove/package.json index ee2ece9db..d5ba95dad 100644 --- a/plugins/remove/package.json +++ b/plugins/remove/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-remove", + "name": "@horizon/plugin-remove", "version": "1.0.0", "description": "Plugin for the 'remove' term in the Horizon Collections API.", "main": "dist/remove.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/replace/package.json b/plugins/replace/package.json index 19afdbec2..729a890f9 100644 --- a/plugins/replace/package.json +++ b/plugins/replace/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-replace", + "name": "@horizon/plugin-replace", "version": "1.0.0", "description": "Plugin for the 'replace' term in the Horizon Collections API.", "main": "dist/replace.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/store/package.json b/plugins/store/package.json index a91b29535..b12f94f67 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-store", + "name": "@horizon/plugin-store", "version": "1.0.0", "description": "Plugin for the 'store' term in the Horizon Collections API.", "main": "dist/store.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json index e26b96748..c38f4194f 100644 --- a/plugins/timeout/package.json +++ b/plugins/timeout/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-timeout", + "name": "@horizon/plugin-timeout", "version": "1.0.0", "description": "Plugin for the 'timeout' term in the Horizon Collections API.", "main": "dist/timeout.js", @@ -28,10 +28,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/update/package.json b/plugins/update/package.json index e358a8f23..921d9c7fd 100644 --- a/plugins/update/package.json +++ b/plugins/update/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-update", + "name": "@horizon/plugin-update", "version": "1.0.0", "description": "Plugin for the 'update' term in the Horizon Collections API.", "main": "dist/update.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/upsert/package.json b/plugins/upsert/package.json index 42405abf4..5dc30c6e9 100644 --- a/plugins/upsert/package.json +++ b/plugins/upsert/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-upsert", + "name": "@horizon/plugin-upsert", "version": "1.0.0", "description": "Plugin for the 'upsert' term in the Horizon Collections API.", "main": "dist/upsert.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/utils/package.json b/plugins/utils/package.json index 75272595a..9e3465792 100644 --- a/plugins/utils/package.json +++ b/plugins/utils/package.json @@ -28,10 +28,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/plugins/watch/package.json b/plugins/watch/package.json index bbdad4a81..c75bed163 100644 --- a/plugins/watch/package.json +++ b/plugins/watch/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugins-watch", + "name": "@horizon/plugin-watch", "version": "1.0.0", "description": "Plugin for the 'watch' term in the Horizon Collections API.", "main": "dist/watch.js", @@ -29,10 +29,10 @@ "@horizon/server": "3.x" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/server/package.json b/server/package.json index 9ba987e23..b86f7233b 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@horizon/server", - "version": "2.0.0-beta-5", + "version": "3.0.0-alpha-0", "description": "Server for RethinkDB Horizon, an open-source developer platform for building realtime, scalable web apps.", "main": "dist/index.js", "scripts": { @@ -23,8 +23,7 @@ "node": ">=4.0.0" }, "dependencies": { - "@horizon/client": "2.0.0-beta-5", - "@horizon/server-utils": "1.0.0", + "@horizon/client": "3.0.0-alpha-0", "bluebird": "^3.4.0", "cookie": "^0.2.3", "joi": "^8.0.4", @@ -37,11 +36,11 @@ "ws": "^1.1.0" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", + "mocha": "^2.5.3", "nodemon": "^1.8.1", - "babel-cli": "^6.10.1", + "babel-cli": "^6.11.4", "source-map-support": "^0.4.0", "babel-preset-es2015": "^6.9.0" } diff --git a/setupDev.sh b/setupDev.sh new file mode 100755 index 000000000..2436541e5 --- /dev/null +++ b/setupDev.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -e + +do_clean=$([ "$1" == "--clean" ]) + +green () { + echo -e "\033[1;32m== $1 \033[0m" +} + +# $1: path to module to link +# $...: dependencies to link into this module +link_dir () { + dir=$1 + shift + + pushd $dir + if [ do_clean = "true" ]; then + echo Removing $dir/node_modules + rm -r node_modules + fi + + green "Unlinking $dir" + npm unlink + green "Linking $dir" + + while (( "$#" )); do + npm link "$1" + shift + done + + npm link --unsafe-perm --cache-min 9999999 + popd +} + +link_dir client +link_dir server "@horizon/client" +link_dir cli "@horizon/server" +link_dir plugins/utils "@horizon/server" + +plugin_names=($(ls -1d plugins/* | grep -v -e utils -e defaults)) +plugin_modules=() +for plugin_name in "${plugin_names[@]}"; do + link_dir "plugins/$plugin_name" "@horizon/server" "@horizon/plugin-utils" + plugin_modules+=("@horizon/plugin-$plugin_name") +done + +link_dir plugins/defaults ${plugin_modules[@]} +link_dir test "@horizon/plugin-defaults" "@horizon/plugin-router" "@horizon/server" "@horizon" + +green "Dev environment ready" diff --git a/test/package.json b/test/package.json index 49047b8da..4411f060b 100644 --- a/test/package.json +++ b/test/package.json @@ -22,17 +22,17 @@ "node": ">=4.0.0" }, "dependencies": { - "@horizon/cli": "3.0.0", + "horizon": "3.0.0", "@horizon/plugin-router": "1.0.0", - "@horizon/plugins": "1.0.0", + "@horizon/plugin-defaults": "1.0.0", "@horizon/server": "3.0.0", "argparse": "^1.0.7" }, "devDependencies": { - "eslint": "^2.3.0", + "eslint": "^3.1.0", "istanbul": "^0.4.3", - "mocha": "^2.3.3", - "babel-cli": "^6.10.1", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", "babel-preset-es2015": "^6.9.0" } } diff --git a/test/serve.js b/test/serve.js index e7e479a76..e59461403 100755 --- a/test/serve.js +++ b/test/serve.js @@ -7,13 +7,13 @@ Error.stackTraceLimit = Infinity; const horizon_server = require('@horizon/server'); const PluginRouter = require('@horizon/plugin-router'); -const horizon_plugins = require('@horizon/plugins'); +const plugins = require('@horizon/plugin-defaults'); // Utilities provided by the CLI library -const each_line_in_pipe = require('@horizon/cli/src/utils/each_line_in_pipe'); -const start_rdb_server = require('@horizon/cli/src/utils/start_rdb_server'); -const rm_sync_recursive = require('@horizon/cli/src/utils/rm_sync_recursive'); -const parse_yes_no_option = require('@horizon/cli/src/utils/parse_yes_no_option'); +const each_line_in_pipe = require('horizon/src/utils/each_line_in_pipe'); +const start_rdb_server = require('horizon/src/utils/start_rdb_server'); +const rm_sync_recursive = require('horizon/src/utils/rm_sync_recursive'); +const parse_yes_no_option = require('horizon/src/utils/parse_yes_no_option'); const assert = require('assert'); const child_process = require('child_process'); @@ -179,29 +179,12 @@ new Promise((resolve) => { }); console.log('starting http servers'); - const plugins = new PluginRouter(hz_server); - Promise.all([ - plugins.add(plugins.collection({ - auto_create_collection: true, - auto_create_index: true, - })), - plugins.add(plugins.permit_all()), - plugins.add(plugins.timeout()), - plugins.add(plugins.insert()), - plugins.add(plugins.store()), - plugins.add(plugins.update()), - plugins.add(plugins.upsert()), - plugins.add(plugins.remove()), - plugins.add(plugins.replace()), - plugins.add(plugins.fetch()), - plugins.add(plugins.watch()), - plugins.add(plugins.above()), - plugins.add(plugins.below()), - plugins.add(plugins.order()), - plugins.add(plugins.limit()), - plugins.add(plugins.find()), - plugins.add(plugins.findAll()), - ]).catch((err) => + const pluginRouter = new PluginRouter(hz_server); + pluginRouter.add(plugins({ + permissions: 'permit-all', + auto_create_collection: true, + auto_create_index: true, + })).catch((err) => console.log(`Plugin initialization failed: ${err.stack}`) ); diff --git a/test/setupDev.sh b/test/setupDev.sh deleted file mode 100755 index 30ec7e14a..000000000 --- a/test/setupDev.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash -set -e - -green () { - echo -e "\033[1;32m== $1 \033[0m" -} - -if [ "$1" == '--clean' ]; then - green 'Removing old node_modules dirs if present' - if [ -d ../client/node_modules ]; then - echo Removing client/node_modules - rm -r ../client/node_modules - fi - if [ -d ../server/node_modules ]; then - echo Removing server/node_modules - rm -r ../server/node_modules - fi - if [ -d ../cli/node_modules ]; then - echo Removing cli/node_modules - rm -r ../cli/node_modules - fi -fi - -pushd ../client -green 'Unlinking existing client' -npm unlink -green 'Linking client' -npm link --unsafe-perm --cache-min 9999999 -popd - -pushd ../server -green 'Unlinking existing server' -npm unlink -green 'Linking server' -npm link @horizon/client -npm link --cache-min 9999999 -popd - -pushd ../cli -green 'Unlinking existing horizon cli' -npm unlink -green 'Linking horizon cli' -npm link @horizon/server -npm link --cache-min 9999999 -popd - -green 'Dev environment set up' From 7eab3106a7c6f4abe1fec09f5e1be7d486085544 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 11:00:59 -0700 Subject: [PATCH 57/86] fixed up setupDev.sh to provision plugins --- setupDev.sh | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/setupDev.sh b/setupDev.sh index 2436541e5..206392b4e 100755 --- a/setupDev.sh +++ b/setupDev.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -e -do_clean=$([ "$1" == "--clean" ]) +if [ "$1" == "--clean" ]; then + do_clean=true +fi green () { echo -e "\033[1;32m== $1 \033[0m" @@ -14,20 +16,21 @@ link_dir () { shift pushd $dir - if [ do_clean = "true" ]; then + if [ "$do_clean" = "true" ]; then echo Removing $dir/node_modules rm -r node_modules fi green "Unlinking $dir" npm unlink - green "Linking $dir" + green "Linking $dir deps" while (( "$#" )); do npm link "$1" shift done + green "Linking $dir" npm link --unsafe-perm --cache-min 9999999 popd } @@ -35,16 +38,22 @@ link_dir () { link_dir client link_dir server "@horizon/client" link_dir cli "@horizon/server" -link_dir plugins/utils "@horizon/server" +link_dir plugin-router -plugin_names=($(ls -1d plugins/* | grep -v -e utils -e defaults)) +# Link all the plugins - 'utils' must go first, and 'defaults' must go last +pushd plugins +link_dir utils "@horizon/server" + +plugin_names=($(ls -1d * | grep -v -e utils -e defaults)) plugin_modules=() for plugin_name in "${plugin_names[@]}"; do - link_dir "plugins/$plugin_name" "@horizon/server" "@horizon/plugin-utils" + link_dir "$plugin_name" "@horizon/server" "@horizon/plugin-utils" plugin_modules+=("@horizon/plugin-$plugin_name") done -link_dir plugins/defaults ${plugin_modules[@]} -link_dir test "@horizon/plugin-defaults" "@horizon/plugin-router" "@horizon/server" "@horizon" +link_dir defaults ${plugin_modules[@]} +popd + +link_dir test "@horizon/plugin-defaults" "@horizon/plugin-router" "@horizon/server" "horizon" green "Dev environment ready" From a78b0db83edbe5e8bb371f381abee2cdfa2fd24b Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 11:28:43 -0700 Subject: [PATCH 58/86] fixing bugs --- plugin-router/src/index.js | 3 --- plugins/collection/src/queries.js | 2 +- test/serve.js | 5 ++--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugin-router/src/index.js b/plugin-router/src/index.js index 0ab42be3c..587a78a9d 100644 --- a/plugin-router/src/index.js +++ b/plugin-router/src/index.js @@ -17,7 +17,6 @@ class PluginRouter extends EventEmitter { noteReady(plugin) { if (!this.readyPlugins.has(plugin)) { - console.log(`noteReady(${plugin}): ${this.readyPlugins.size}/${this.plugins.size}`); this.readyPlugins.add(plugin); this.emit('pluginReady', plugin, this); if (this.readyPlugins.size === this.plugins.size) { @@ -28,7 +27,6 @@ class PluginRouter extends EventEmitter { noteUnready(plugin) { if (this.readyPlugins.has(plugin)) { - console.log(`noteUnready(${plugin}): ${this.readyPlugins.size}/${this.plugins.size}`); this.readyPlugins.delete(plugin); this.emit('pluginUnready', plugin, this); if (this.readyPlugins.size === this.plugins.size - 1) { @@ -47,7 +45,6 @@ class PluginRouter extends EventEmitter { this.plugins.set(plugin.name, Promise.resolve(this.server).then((server) => { this.emit('unready', this); if (plugin.activate.length > 1) { - console.log(`activating ${plugin.name} with ready callbacks`); return plugin.activate( server, () => this.noteReady(plugin.name), diff --git a/plugins/collection/src/queries.js b/plugins/collection/src/queries.js index 5a77205f3..b0411e6bb 100644 --- a/plugins/collection/src/queries.js +++ b/plugins/collection/src/queries.js @@ -29,7 +29,7 @@ function initializeMetadata(db, conn) { .then(() => Promise.all([ r.db(db).tableList().contains('users').not().run(conn).then(() => - createCollection(r, db, 'users', conn)), + createCollection(db, 'users', conn)), r.db(db).table('hz_collections') .insert({id: 'hz_metadata', version: metadataVersion}) .run(conn), diff --git a/test/serve.js b/test/serve.js index e59461403..e4d7da38d 100755 --- a/test/serve.js +++ b/test/serve.js @@ -188,9 +188,8 @@ new Promise((resolve) => { console.log(`Plugin initialization failed: ${err.stack}`) ); - plugins.once('ready', () => { - console.log('READY OMGZZZZ'); - hz_server.set_middleware(plugins.hzMiddleware()); + pluginRouter.once('ready', () => { + hz_server.set_middleware(pluginRouter.hzMiddleware()); // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server http_servers.forEach((serv, i) => { From 1615b7a4212dd0fe443d2e8830340607d416faae Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 2 Sep 2016 11:41:41 -0700 Subject: [PATCH 59/86] fixed incorrect error handling in collection creation --- plugins/collection/src/types/metadata.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/collection/src/types/metadata.js b/plugins/collection/src/types/metadata.js index 24bca9dcb..593f99d8c 100644 --- a/plugins/collection/src/types/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -330,9 +330,9 @@ class ReliableMetadata extends Reliable { return new Promise((resolve, reject) => collection._on_ready((maybeErr) => { if (maybeErr instanceof Error) { - resolve(collection); - } else { reject(maybeErr); + } else { + resolve(collection); } })); }).catch((err) => { From 43c921aa2ae4bde9331643bc9f74b302071d0442 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Sat, 3 Sep 2016 01:21:01 +0000 Subject: [PATCH 60/86] about to fix namespaces --- cli/package.json | 2 +- {plugins/utils => plugin-utils}/.babelrc | 0 {plugins/utils => plugin-utils}/package.json | 0 {plugins/utils => plugin-utils}/src/reads.js | 0 {plugins/utils => plugin-utils}/src/utils.js | 2 +- {plugins/utils => plugin-utils}/src/writes.js | 0 symlinkDev.sh | 18 ++++++++++++++++++ 7 files changed, 20 insertions(+), 2 deletions(-) rename {plugins/utils => plugin-utils}/.babelrc (100%) rename {plugins/utils => plugin-utils}/package.json (100%) rename {plugins/utils => plugin-utils}/src/reads.js (100%) rename {plugins/utils => plugin-utils}/src/utils.js (96%) rename {plugins/utils => plugin-utils}/src/writes.js (100%) create mode 100644 symlinkDev.sh diff --git a/cli/package.json b/cli/package.json index 91d0eccef..771ed89f3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -23,7 +23,7 @@ }, "homepage": "https://github.com/rethinkdb/horizon#readme", "dependencies": { - "@horizon/server": "2.0.0-beta-5", + "@horizon/server": "3.0.0-alpha-0", "argparse": "^1.0.3", "bluebird": "^3.4.1", "chalk": "^1.1.3", diff --git a/plugins/utils/.babelrc b/plugin-utils/.babelrc similarity index 100% rename from plugins/utils/.babelrc rename to plugin-utils/.babelrc diff --git a/plugins/utils/package.json b/plugin-utils/package.json similarity index 100% rename from plugins/utils/package.json rename to plugin-utils/package.json diff --git a/plugins/utils/src/reads.js b/plugin-utils/src/reads.js similarity index 100% rename from plugins/utils/src/reads.js rename to plugin-utils/src/reads.js diff --git a/plugins/utils/src/utils.js b/plugin-utils/src/utils.js similarity index 96% rename from plugins/utils/src/utils.js rename to plugin-utils/src/utils.js index 08e7278a8..1d76248af 100644 --- a/plugins/utils/src/utils.js +++ b/plugin-utils/src/utils.js @@ -43,7 +43,7 @@ function remakeError(err) { } function isObject(x) { - return !Array.isArray(x) && x !== null; + return typeof x === 'object' && !Array.isArray(x) && x !== null; } const reqlOptions = { diff --git a/plugins/utils/src/writes.js b/plugin-utils/src/writes.js similarity index 100% rename from plugins/utils/src/writes.js rename to plugin-utils/src/writes.js diff --git a/symlinkDev.sh b/symlinkDev.sh new file mode 100644 index 000000000..43a8c57da --- /dev/null +++ b/symlinkDev.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +for i in plugins/*; do + { + cd $i + plugins=`cat package.json \ + | perl -n -e '/^ *"\@horizon\/plugin-([^"]*)\"/ && print "$1\n"' \ + | grep -v router`; + mkdir -p node_modules/@horizon + cd node_modules/@horizon + ln -f -n -s ../../../../server server + for p in $plugins; do + ln -f -n -s ../../../$p plugin-$p + done + } & +done + +wait From f5a8415ba7bc71cf3dd0c844086f3ad59dadde94 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Sat, 3 Sep 2016 01:25:42 +0000 Subject: [PATCH 61/86] fixed namespacing --- plugins/above/package.json | 2 +- plugins/below/package.json | 2 +- plugins/collection/package.json | 2 +- plugins/defaults/package.json | 38 ++++++++++++++++---------------- plugins/fetch/package.json | 2 +- plugins/find/package.json | 2 +- plugins/findAll/package.json | 2 +- plugins/insert/package.json | 2 +- plugins/limit/package.json | 2 +- plugins/order/package.json | 2 +- plugins/permissions/package.json | 2 +- plugins/permit-all/package.json | 2 +- plugins/remove/package.json | 2 +- plugins/replace/package.json | 2 +- plugins/store/package.json | 2 +- plugins/timeout/package.json | 2 +- plugins/update/package.json | 2 +- plugins/upsert/package.json | 2 +- plugins/watch/package.json | 2 +- test/package.json | 2 +- 20 files changed, 38 insertions(+), 38 deletions(-) diff --git a/plugins/above/package.json b/plugins/above/package.json index f7fd4e442..f60609ce8 100644 --- a/plugins/above/package.json +++ b/plugins/above/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-above", + "name": "@horizon-plugins/above", "version": "1.0.0", "description": "Plugin for the 'above' term in the Horizon Collections API.", "main": "dist/above.js", diff --git a/plugins/below/package.json b/plugins/below/package.json index 34ff8b9b4..71930f854 100644 --- a/plugins/below/package.json +++ b/plugins/below/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-below", + "name": "@horizon-plugins/below", "version": "1.0.0", "description": "Plugin for the 'below' term in the Horizon Collections API.", "main": "dist/below.js", diff --git a/plugins/collection/package.json b/plugins/collection/package.json index a3a181af4..352a70e1c 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-collection", + "name": "@horizon-plugins/collection", "version": "1.0.0", "description": "Plugin for the 'collection' term in the Horizon Collections API.", "main": "dist/collection.js", diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json index fde3608e1..4956ca237 100644 --- a/plugins/defaults/package.json +++ b/plugins/defaults/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-defaults", + "name": "@horizon-plugins/defaults", "version": "1.0.0", "description": "Contains all the default plugins for the Horizon Collections API.", "main": "dist/defaults.js", @@ -22,24 +22,24 @@ "node": ">=4.0.0" }, "dependencies": { - "@horizon/plugin-above": "1.0.0", - "@horizon/plugin-below": "1.0.0", - "@horizon/plugin-collection": "1.0.0", - "@horizon/plugin-fetch": "1.0.0", - "@horizon/plugin-find": "1.0.0", - "@horizon/plugin-findAll": "1.0.0", - "@horizon/plugin-insert": "1.0.0", - "@horizon/plugin-limit": "1.0.0", - "@horizon/plugin-order": "1.0.0", - "@horizon/plugin-permissions": "1.0.0", - "@horizon/plugin-permit-all": "1.0.0", - "@horizon/plugin-remove": "1.0.0", - "@horizon/plugin-replace": "1.0.0", - "@horizon/plugin-store": "1.0.0", - "@horizon/plugin-timeout": "1.0.0", - "@horizon/plugin-update": "1.0.0", - "@horizon/plugin-upsert": "1.0.0", - "@horizon/plugin-watch": "1.0.0" + "@horizon-plugins/above": "1.0.0", + "@horizon-plugins/below": "1.0.0", + "@horizon-plugins/collection": "1.0.0", + "@horizon-plugins/fetch": "1.0.0", + "@horizon-plugins/find": "1.0.0", + "@horizon-plugins/findAll": "1.0.0", + "@horizon-plugins/insert": "1.0.0", + "@horizon-plugins/limit": "1.0.0", + "@horizon-plugins/order": "1.0.0", + "@horizon-plugins/permissions": "1.0.0", + "@horizon-plugins/permit-all": "1.0.0", + "@horizon-plugins/remove": "1.0.0", + "@horizon-plugins/replace": "1.0.0", + "@horizon-plugins/store": "1.0.0", + "@horizon-plugins/timeout": "1.0.0", + "@horizon-plugins/update": "1.0.0", + "@horizon-plugins/upsert": "1.0.0", + "@horizon-plugins/watch": "1.0.0" }, "peerDependencies": { "@horizon/plugin-router": "1.x", diff --git a/plugins/fetch/package.json b/plugins/fetch/package.json index e174b2c0d..fb09532b3 100644 --- a/plugins/fetch/package.json +++ b/plugins/fetch/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-fetch", + "name": "@horizon-plugins/fetch", "version": "1.0.0", "description": "Plugin for the 'fetch' term in the Horizon Collections API.", "main": "dist/fetch.js", diff --git a/plugins/find/package.json b/plugins/find/package.json index f2198e0ff..03df7f724 100644 --- a/plugins/find/package.json +++ b/plugins/find/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-find", + "name": "@horizon-plugins/find", "version": "1.0.0", "description": "Plugin for the 'find' term in the Horizon Collections API.", "main": "dist/find.js", diff --git a/plugins/findAll/package.json b/plugins/findAll/package.json index f0da8be8f..bf2bbedb9 100644 --- a/plugins/findAll/package.json +++ b/plugins/findAll/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-findAll", + "name": "@horizon-plugins/findAll", "version": "1.0.0", "description": "Plugin for the 'findAll' term in the Horizon Collections API.", "main": "dist/findAll.js", diff --git a/plugins/insert/package.json b/plugins/insert/package.json index b6a7e55f1..a852def6d 100644 --- a/plugins/insert/package.json +++ b/plugins/insert/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-insert", + "name": "@horizon-plugins/insert", "version": "1.0.0", "description": "Plugin for the 'insert' term in the Horizon Collections API.", "main": "dist/insert.js", diff --git a/plugins/limit/package.json b/plugins/limit/package.json index 809e9e058..7c926757b 100644 --- a/plugins/limit/package.json +++ b/plugins/limit/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-limit", + "name": "@horizon-plugins/limit", "version": "1.0.0", "description": "Plugin for the 'limit' term in the Horizon Collections API.", "main": "dist/limit.js", diff --git a/plugins/order/package.json b/plugins/order/package.json index c44f4e4c1..df98d67d8 100644 --- a/plugins/order/package.json +++ b/plugins/order/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-order", + "name": "@horizon-plugins/order", "version": "1.0.0", "description": "Plugin for the 'order' term in the Horizon Collections API.", "main": "dist/order.js", diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index cdac6af69..e7bac08bc 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-permissions", + "name": "@horizon-plugins/permissions", "version": "1.0.0", "description": "Plugin adding user groups and permissions to Horizon.", "main": "dist/permissions.js", diff --git a/plugins/permit-all/package.json b/plugins/permit-all/package.json index b9850f479..d0ca5a8b2 100644 --- a/plugins/permit-all/package.json +++ b/plugins/permit-all/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-permit-all", + "name": "@horizon-plugins/permit-all", "version": "1.0.0", "description": "Plugin allowing any request to run without permissions in Horizon.", "main": "dist/permit-all.js", diff --git a/plugins/remove/package.json b/plugins/remove/package.json index d5ba95dad..b4243eddd 100644 --- a/plugins/remove/package.json +++ b/plugins/remove/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-remove", + "name": "@horizon-plugins/remove", "version": "1.0.0", "description": "Plugin for the 'remove' term in the Horizon Collections API.", "main": "dist/remove.js", diff --git a/plugins/replace/package.json b/plugins/replace/package.json index 729a890f9..ffbe1ec9a 100644 --- a/plugins/replace/package.json +++ b/plugins/replace/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-replace", + "name": "@horizon-plugins/replace", "version": "1.0.0", "description": "Plugin for the 'replace' term in the Horizon Collections API.", "main": "dist/replace.js", diff --git a/plugins/store/package.json b/plugins/store/package.json index b12f94f67..6f76272ba 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-store", + "name": "@horizon-plugins/store", "version": "1.0.0", "description": "Plugin for the 'store' term in the Horizon Collections API.", "main": "dist/store.js", diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json index c38f4194f..48dc6d99c 100644 --- a/plugins/timeout/package.json +++ b/plugins/timeout/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-timeout", + "name": "@horizon-plugins/timeout", "version": "1.0.0", "description": "Plugin for the 'timeout' term in the Horizon Collections API.", "main": "dist/timeout.js", diff --git a/plugins/update/package.json b/plugins/update/package.json index 921d9c7fd..241097255 100644 --- a/plugins/update/package.json +++ b/plugins/update/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-update", + "name": "@horizon-plugins/update", "version": "1.0.0", "description": "Plugin for the 'update' term in the Horizon Collections API.", "main": "dist/update.js", diff --git a/plugins/upsert/package.json b/plugins/upsert/package.json index 5dc30c6e9..9303403aa 100644 --- a/plugins/upsert/package.json +++ b/plugins/upsert/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-upsert", + "name": "@horizon-plugins/upsert", "version": "1.0.0", "description": "Plugin for the 'upsert' term in the Horizon Collections API.", "main": "dist/upsert.js", diff --git a/plugins/watch/package.json b/plugins/watch/package.json index c75bed163..0f5b95eb3 100644 --- a/plugins/watch/package.json +++ b/plugins/watch/package.json @@ -1,5 +1,5 @@ { - "name": "@horizon/plugin-watch", + "name": "@horizon-plugins/watch", "version": "1.0.0", "description": "Plugin for the 'watch' term in the Horizon Collections API.", "main": "dist/watch.js", diff --git a/test/package.json b/test/package.json index 4411f060b..884cadc16 100644 --- a/test/package.json +++ b/test/package.json @@ -24,7 +24,7 @@ "dependencies": { "horizon": "3.0.0", "@horizon/plugin-router": "1.0.0", - "@horizon/plugin-defaults": "1.0.0", + "@horizon-plugins/defaults": "1.0.0", "@horizon/server": "3.0.0", "argparse": "^1.0.7" }, From a1dfb8778415c7521baaf486038bf25f0430d355 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 4 Sep 2016 18:19:43 -0700 Subject: [PATCH 62/86] fixing bugs --- plugins/collection/src/types/collection.js | 7 +++++++ server/src/schema/horizon_protocol.js | 14 +++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/collection/src/types/collection.js b/plugins/collection/src/types/collection.js index ea676328e..4936ba01d 100644 --- a/plugins/collection/src/types/collection.js +++ b/plugins/collection/src/types/collection.js @@ -75,6 +75,13 @@ class Collection { return this._get_table().create_index(fields, this.reliableConn.connection(), done); } + ready() { + if (this._tables.size === 0) { + return false; + } + return this._get_table().ready(); + } + get_matching_index(fuzzy_fields, ordered_fields) { return new Promise((resolve, reject) => { const done = (indexOrErr) => { diff --git a/server/src/schema/horizon_protocol.js b/server/src/schema/horizon_protocol.js index 17943c481..4eb1a7489 100644 --- a/server/src/schema/horizon_protocol.js +++ b/server/src/schema/horizon_protocol.js @@ -8,17 +8,13 @@ const handshake = Joi.object().keys({ token: Joi.string().required() .when('method', {is: Joi.not('token').required(), then: Joi.forbidden()}), }).unknown(false); -// RSI: get this working again -// const request = Joi.object({ -// request_id: Joi.number().required(), -// type: Joi.only('end_subscription', 'keepalive').optional(), -// options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required() -// .when('type', {is: Joi.string().only('end_subscription', 'keepalive'), then: Joi.forbidden()}) -// }).unknown(false); -// + const request = Joi.object({ request_id: Joi.number().required(), - options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required(), + type: Joi.only('end_subscription', 'keepalive').optional(), + options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required() + .when('type', {is: Joi.string().only('end_subscription', 'keepalive').required(), + then: Joi.forbidden()}) }).unknown(false); module.exports = { From 16575caa64976b82489918a7c066026237b61efe Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 4 Sep 2016 18:55:23 -0700 Subject: [PATCH 63/86] about to start moving and writing tests --- plugins/permissions/package.json | 2 +- server/package.json | 2 +- server/{src => }/test/permissions.js | 0 server/{src => }/test/prereq_tests.js | 0 server/{src => }/test/protocol_tests.js | 0 server/{src => }/test/query_tests.js | 0 server/{src => }/test/schema.js | 0 server/{src => }/test/subscribe_tests.js | 0 server/{src => }/test/test.js | 0 server/{src => }/test/utils.js | 0 server/{src => }/test/write_tests.js | 0 11 files changed, 2 insertions(+), 2 deletions(-) rename server/{src => }/test/permissions.js (100%) rename server/{src => }/test/prereq_tests.js (100%) rename server/{src => }/test/protocol_tests.js (100%) rename server/{src => }/test/query_tests.js (100%) rename server/{src => }/test/schema.js (100%) rename server/{src => }/test/subscribe_tests.js (100%) rename server/{src => }/test/test.js (100%) rename server/{src => }/test/utils.js (100%) rename server/{src => }/test/write_tests.js (100%) diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index cdac6af69..9ab323fa8 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -5,7 +5,7 @@ "main": "dist/permissions.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", + "test": "mocha --compilers js:babel-register --require babel-polyfill test", "build": "babel src -d dist -s true" }, "repository": { diff --git a/server/package.json b/server/package.json index b86f7233b..b73b592b4 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "lint": "eslint src test", - "test": "mocha dist/test --timeout 10000", + "test": "mocha --compilers js:babel-register --require babel-polyfill test --timeout 10000", "coverage": "istanbul cover _mocha test/test.js", "build": "babel src -d dist -s true" }, diff --git a/server/src/test/permissions.js b/server/test/permissions.js similarity index 100% rename from server/src/test/permissions.js rename to server/test/permissions.js diff --git a/server/src/test/prereq_tests.js b/server/test/prereq_tests.js similarity index 100% rename from server/src/test/prereq_tests.js rename to server/test/prereq_tests.js diff --git a/server/src/test/protocol_tests.js b/server/test/protocol_tests.js similarity index 100% rename from server/src/test/protocol_tests.js rename to server/test/protocol_tests.js diff --git a/server/src/test/query_tests.js b/server/test/query_tests.js similarity index 100% rename from server/src/test/query_tests.js rename to server/test/query_tests.js diff --git a/server/src/test/schema.js b/server/test/schema.js similarity index 100% rename from server/src/test/schema.js rename to server/test/schema.js diff --git a/server/src/test/subscribe_tests.js b/server/test/subscribe_tests.js similarity index 100% rename from server/src/test/subscribe_tests.js rename to server/test/subscribe_tests.js diff --git a/server/src/test/test.js b/server/test/test.js similarity index 100% rename from server/src/test/test.js rename to server/test/test.js diff --git a/server/src/test/utils.js b/server/test/utils.js similarity index 100% rename from server/src/test/utils.js rename to server/test/utils.js diff --git a/server/src/test/write_tests.js b/server/test/write_tests.js similarity index 100% rename from server/src/test/write_tests.js rename to server/test/write_tests.js From 0a26b5093798499fb707948233863b5b3362a4a8 Mon Sep 17 00:00:00 2001 From: Michael Lucy Date: Mon, 5 Sep 2016 02:52:08 +0000 Subject: [PATCH 64/86] stuffs --- symlinkDev.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/symlinkDev.sh b/symlinkDev.sh index 43a8c57da..b40d6c2ee 100644 --- a/symlinkDev.sh +++ b/symlinkDev.sh @@ -4,13 +4,17 @@ for i in plugins/*; do { cd $i plugins=`cat package.json \ - | perl -n -e '/^ *"\@horizon\/plugin-([^"]*)\"/ && print "$1\n"' \ - | grep -v router`; + | perl -n -e '/^ *"\@horizon-plugins\/([^"]*)\"/ && print "$1\n"'`; mkdir -p node_modules/@horizon + mkdir -p node_modules/@horizon-plugins + cd node_modules/@horizon ln -f -n -s ../../../../server server + ln -f -n -s ../../../../plugin-utils plugin-utils + + cd ../@horizon-plugins for p in $plugins; do - ln -f -n -s ../../../$p plugin-$p + ln -f -n -s ../../../$p $p done } & done From 7845099e8f428a92c02e7458033643a33931562e Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 4 Sep 2016 23:22:01 -0700 Subject: [PATCH 65/86] migrated permissions template tests --- babelify.sh | 2 +- plugin-utils/src/test.js | 25 +++ plugin-utils/src/utils.js | 1 + plugins/defaults/src/defaults.js | 36 ++-- plugins/permissions/package.json | 8 +- plugins/permissions/src/rule.js | 6 +- plugins/permissions/src/test/template.js | 233 +++++++++++++++++++++++ server/src/response.js | 2 +- setupDev.sh | 21 +- test/package.json | 4 +- 10 files changed, 301 insertions(+), 37 deletions(-) create mode 100644 plugin-utils/src/test.js create mode 100644 plugins/permissions/src/test/template.js diff --git a/babelify.sh b/babelify.sh index 3fdaf6a1f..c58e73d3e 100755 --- a/babelify.sh +++ b/babelify.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -dirs="server plugins plugin-router" +dirs="server plugins plugin-router plugin-utils" for path in `find $dirs -name .babelrc | grep -v node_modules`; do { babel ${path%%.babelrc}/src -d ${path%%.babelrc}/dist -s true -w diff --git a/plugin-utils/src/test.js b/plugin-utils/src/test.js new file mode 100644 index 000000000..b3a4bab55 --- /dev/null +++ b/plugin-utils/src/test.js @@ -0,0 +1,25 @@ +'use strict'; + +const Response = require('@horizon/server/dist/response'); + +// Mock Response object for use by tests +class MockResponse extends Response { + constructor() { + super((obj) => { + if (obj.data) { + for (const item of obj.data) { + this._data.push(item); + } + } + this._messages.push(obj); + }); + + this._data = []; + this._messages = []; + + this.data = this.complete.then(() => this._data); + this.messages = this.complete.then(() => this._messages); + } +}; + +module.exports = {MockResponse}; diff --git a/plugin-utils/src/utils.js b/plugin-utils/src/utils.js index 1d76248af..f97d0741f 100644 --- a/plugin-utils/src/utils.js +++ b/plugin-utils/src/utils.js @@ -61,4 +61,5 @@ module.exports = { versionField, reads: require('./reads'), writes: require('./writes'), + test: require('./test'), }; diff --git a/plugins/defaults/src/defaults.js b/plugins/defaults/src/defaults.js index 2a899c005..b98a734cb 100644 --- a/plugins/defaults/src/defaults.js +++ b/plugins/defaults/src/defaults.js @@ -2,28 +2,28 @@ // Collections API const defaultMethods = { - above: require('@horizon/plugin-above'), - below: require('@horizon/plugin-below'), - collection: require('@horizon/plugin-collection'), - insert: require('@horizon/plugin-insert'), - fetch: require('@horizon/plugin-fetch'), - find: require('@horizon/plugin-find'), - findAll: require('@horizon/plugin-findAll'), - limit: require('@horizon/plugin-limit'), - order: require('@horizon/plugin-order'), - remove: require('@horizon/plugin-remove'), - replace: require('@horizon/plugin-replace'), - store: require('@horizon/plugin-store'), - timeout: require('@horizon/plugin-timeout'), - update: require('@horizon/plugin-update'), - upsert: require('@horizon/plugin-upsert'), - watch: require('@horizon/plugin-watch'), + above: require('@horizon-plugins/above'), + below: require('@horizon-plugins/below'), + collection: require('@horizon-plugins/collection'), + insert: require('@horizon-plugins/insert'), + fetch: require('@horizon-plugins/fetch'), + find: require('@horizon-plugins/find'), + findAll: require('@horizon-plugins/findAll'), + limit: require('@horizon-plugins/limit'), + order: require('@horizon-plugins/order'), + remove: require('@horizon-plugins/remove'), + replace: require('@horizon-plugins/replace'), + store: require('@horizon-plugins/store'), + timeout: require('@horizon-plugins/timeout'), + update: require('@horizon-plugins/update'), + upsert: require('@horizon-plugins/upsert'), + watch: require('@horizon-plugins/watch'), }; // Permissions API const defaultPermissions = { - permissions: require('@horizon/plugin-permissions'), - 'permit-all': require('@horizon/plugin-permit-all'), + permissions: require('@horizon-plugins/permissions'), + 'permit-all': require('@horizon-plugins/permit-all'), }; // Combines some subset of the default plugins into a single plugin for ease-of-use diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index deab64dda..e83bdcd9a 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -5,7 +5,7 @@ "main": "dist/permissions.js", "scripts": { "lint": "eslint src", - "test": "mocha --compilers js:babel-register --require babel-polyfill test", + "test": "mocha dist/test", "build": "babel src -d dist -s true" }, "repository": { @@ -22,7 +22,8 @@ "node": ">=4.0.0" }, "dependencies": { - "@horizon/plugin-utils": "1.0.0" + "@horizon/plugin-utils": "1.0.0", + "@horizon/client": "3.0.0-alpha-0" }, "peerDependencies": { "@horizon/plugin-router": "1.x", @@ -33,6 +34,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/permissions/src/rule.js b/plugins/permissions/src/rule.js index f24175c9d..48c44f072 100644 --- a/plugins/permissions/src/rule.js +++ b/plugins/permissions/src/rule.js @@ -12,14 +12,14 @@ class Rule { } is_match(query, context) { - return this._template.is_match(query, context); + return this.template.is_match(query, context); } is_valid(...args) { - if (!this._validator) { + if (!this.validator) { return true; } - return this._validator.is_valid(...args); + return this.validator.is_valid(...args); } } diff --git a/plugins/permissions/src/test/template.js b/plugins/permissions/src/test/template.js new file mode 100644 index 000000000..dc1e6905a --- /dev/null +++ b/plugins/permissions/src/test/template.js @@ -0,0 +1,233 @@ +'use strict'; + +require('source-map-support').install(); + +const Rule = require('../rule'); +const Validator = require('../validator'); + +const assert = require('assert'); + +const make_request = (type, collection, options) => { + if (collection !== null) { + return {request_id: 5, type, options: Object.assign({collection}, options)}; + } else { + return {request_id: 5, type, options}; + } +}; + +const context = {id: 3, groups: ['admin', 'default', 'authenticated']}; + +describe('Template', () => { + it('any', () => { + const rule = new Rule({template: 'any()'}); + + const tests = [{ }, + {type: 'query', options: {collection: 'test'}}, + {fake: 'bar'}, + {options: { }}, + {type: 'query', options: {fake: 'baz'}}]; + + for (const t of tests) { + assert(rule.is_match(t, context)); + assert(rule.is_valid()); + } + }); + + it('any read', () => { + const rule = new Rule({template: 'collection(any()).anyRead()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('fake', 'test', { }), context)); + assert(!rule.is_match(make_request('store', 'test', { }), context)); + assert(!rule.is_match(make_request('query', null, { }), context)); + assert(rule.is_match(make_request('query', 'fake', { }), context)); + assert(rule.is_match(make_request('query', 'fake', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }, { }]}), context)); + assert(!rule.is_match(make_request('subscribe', null, { }), context)); + assert(rule.is_match(make_request('subscribe', 'fake', { }), context)); + assert(rule.is_match(make_request('subscribe', 'fake', {find: { }}), context)); + assert(rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); + assert(rule.is_match(make_request('subscribe', 'test', {find_all: [{ }, { }]}), context)); + }); + + it('any read with collection', () => { + const rule = new Rule({template: 'collection("test").anyRead()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'fake', { }), context)); + assert(rule.is_match(make_request('query', 'test', { }), context)); + assert(rule.is_match(make_request('query', 'test', { }), context)); + assert(rule.is_match(make_request('query', 'test', { }), context)); + assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + }); + + it('any read with order', () => { + // TODO: allow for any number of fields in order + const rule = new Rule({template: 'collection("test").order(any(), any()).anyRead()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'fake', {order: ['foo', 'ascending']}), context)); + assert(!rule.is_match(make_request('query', 'test', { }), context)); + assert(!rule.is_match(make_request('query', 'test', {order: ['baz']}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: ['baz', 'fake']}), context)); + assert(!rule.is_match(make_request('query', 'test', {order: [['fake']]}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [['foo'], 'ascending']}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [['bar'], 'descending']}), context)); + assert(rule.is_match(make_request('query', 'test', {order: [['baz'], 'fake']}), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); + assert(rule.is_match(make_request('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); + }); + + it('any read with find', () => { + const rule = new Rule({template: 'collection("test").find(any()).anyRead()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'fake', {find: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', { }), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {find: { }, fake: 'baz'}), context)); + }); + + it('any read with findAll', () => { + // TODO: allow for any number of arguments in findAll + const rule = new Rule({template: 'collection("test").findAll(any()).anyRead()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'fake', {find_all: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', { }), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); + }); + + it('single key in findAll', () => { + const rule = new Rule({template: 'collection("test").findAll({ owner: userId() }).fetch()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id}, {other: context.id}]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id}]}), context)); + }); + + it('multiple keys in findAll', () => { + const rule = new Rule({template: 'collection("test").findAll({ owner: userId(), key: any() }).fetch()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, key: 3}]}), context)); + }); + + it('multiple items in findAll', () => { + const rule = new Rule({template: 'collection("test").findAll({ a: userId() }, { b: userId() })'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: (context.id + 1)}, {b: context.id}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id, bar: 'baz'}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id, b: context.id}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{b: context.id}]}), context)); + assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id, bar: 'baz'}]}), context)); + assert(rule.is_match(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id}]}), context)); + }); + + it('collection fetch', () => { + const rule = new Rule({template: 'collection("test").fetch()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'fake', { }), context)); + assert(!rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {id: 5}}), context)); + assert(rule.is_match(make_request('query', 'test', { }), context)); + }); + + it('collection watch', () => { + const rule = new Rule({template: 'collection("test").watch()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('subscribe', 'fake', { }), context)); + assert(!rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); + assert(!rule.is_match(make_request('subscribe', 'test', {find: {id: 5}}), context)); + assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + }); + + for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { + it(`collection ${type}`, () => { + const rule = new Rule({template: `collection("test").${type}(any())`}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request(type, 'test', { }), context)); + assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: []}), context)); + assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); + }); + it(`collection ${type} batch`, () => { + const rule = new Rule({template: `collection("test").${type}(anyArray(any()))`}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request(type, 'test', { }), context)); + assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); + assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); + assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: []}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); + }); + } + + it('any write', () => { + const rule = new Rule({template: 'collection("test").anyWrite()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('fake', 'test', { }), context)); + assert(!rule.is_match(make_request('query', 'test', { }), context)); + assert(!rule.is_match(make_request('store', null, { }), context)); + + for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { + assert(!rule.is_match(make_request(type, 'fake', { }), context)); + assert(rule.is_match(make_request(type, 'test', {data: []}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.is_match(make_request(type, 'test', {data: [], bar: 'baz'}), context)); + } + }); + + it('userId in find', () => { + const rule = new Rule({template: 'collection("test").find({ owner: userId() }).fetch()'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: true}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: []}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); + assert(!rule.is_match(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); + assert(rule.is_match(make_request('query', 'test', {find: {owner: context.id}}), context)); + }); + + it('adds readAny() implicitly', () => { + { + const rule = new Rule({template: 'collection("test")'}); + assert(rule.is_valid()); + assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + } + { + const rule = new Rule({template: 'collection("test").find({bar: any()})'}); + assert(rule.is_valid()); + assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); + assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + } + }); + + it('error on incomplete template', () => { + assert.throws(() => new Rule({template: '({ })'}), /Incomplete template/); + assert.throws(() => new Rule({template: '[ ]'}), /Invalid template/); + assert.throws(() => new Rule({template: '5'}), /Invalid template/); + assert.throws(() => new Rule({template: 'null'}), /Invalid template/); + }); +}); diff --git a/server/src/response.js b/server/src/response.js index c3fce2d6f..d74addb92 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -28,7 +28,7 @@ class Response { } else if (!this._completed && state === 'complete') { throw new Error( '`.write()` cannot be used to send a `state: complete` message.' + - ' Use `.end()` to complete a Response.'); + ' Use `.end()` to complete a Response.'); } this._socketSend({state, data}); } diff --git a/setupDev.sh b/setupDev.sh index 206392b4e..2500c9609 100755 --- a/setupDev.sh +++ b/setupDev.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -if [ "$1" == "--clean" ]; then +if [[ "$1" == "--clean" ]]; then do_clean=true fi @@ -16,9 +16,9 @@ link_dir () { shift pushd $dir - if [ "$do_clean" = "true" ]; then + if [[ "$do_clean" == "true" ]]; then echo Removing $dir/node_modules - rm -r node_modules + rm -rf node_modules fi green "Unlinking $dir" @@ -39,21 +39,24 @@ link_dir client link_dir server "@horizon/client" link_dir cli "@horizon/server" link_dir plugin-router +link_dir plugin-utils # Link all the plugins - 'utils' must go first, and 'defaults' must go last pushd plugins -link_dir utils "@horizon/server" - -plugin_names=($(ls -1d * | grep -v -e utils -e defaults)) +plugin_names=($(ls -1d * | grep -v defaults)) plugin_modules=() for plugin_name in "${plugin_names[@]}"; do - link_dir "$plugin_name" "@horizon/server" "@horizon/plugin-utils" - plugin_modules+=("@horizon/plugin-$plugin_name") + link_dir "$plugin_name" \ + "@horizon/server" \ + "@horizon/plugin-utils" \ + "@horizon/client" \ + "@horizon/plugin-router" + plugin_modules+=("@horizon-plugins/$plugin_name") done link_dir defaults ${plugin_modules[@]} popd -link_dir test "@horizon/plugin-defaults" "@horizon/plugin-router" "@horizon/server" "horizon" +link_dir test "@horizon-plugins/defaults" "@horizon/plugin-router" "@horizon/server" "horizon" green "Dev environment ready" diff --git a/test/package.json b/test/package.json index 884cadc16..1bd77685f 100644 --- a/test/package.json +++ b/test/package.json @@ -22,10 +22,10 @@ "node": ">=4.0.0" }, "dependencies": { - "horizon": "3.0.0", + "horizon": "3.0.0-alpha-0", "@horizon/plugin-router": "1.0.0", "@horizon-plugins/defaults": "1.0.0", - "@horizon/server": "3.0.0", + "@horizon/server": "3.0.0-alpha-0", "argparse": "^1.0.7" }, "devDependencies": { From ec572f1a3d6e9b9d36a08cecdc9ff4b949c786fd Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 4 Sep 2016 23:25:36 -0700 Subject: [PATCH 66/86] adding source-map-support devDependency to plugins --- plugins/above/package.json | 3 ++- plugins/below/package.json | 3 ++- plugins/collection/package.json | 3 ++- plugins/defaults/package.json | 3 ++- plugins/fetch/package.json | 3 ++- plugins/find/package.json | 3 ++- plugins/findAll/package.json | 3 ++- plugins/insert/package.json | 3 ++- plugins/limit/package.json | 3 ++- plugins/order/package.json | 3 ++- plugins/permit-all/package.json | 3 ++- plugins/remove/package.json | 3 ++- plugins/replace/package.json | 3 ++- plugins/store/package.json | 3 ++- plugins/timeout/package.json | 3 ++- plugins/update/package.json | 3 ++- plugins/upsert/package.json | 3 ++- plugins/watch/package.json | 3 ++- 18 files changed, 36 insertions(+), 18 deletions(-) diff --git a/plugins/above/package.json b/plugins/above/package.json index f60609ce8..ffcf1ed3b 100644 --- a/plugins/above/package.json +++ b/plugins/above/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/below/package.json b/plugins/below/package.json index 71930f854..5302d247b 100644 --- a/plugins/below/package.json +++ b/plugins/below/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/collection/package.json b/plugins/collection/package.json index 352a70e1c..ec044fb6f 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json index 4956ca237..a177e760b 100644 --- a/plugins/defaults/package.json +++ b/plugins/defaults/package.json @@ -50,6 +50,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/fetch/package.json b/plugins/fetch/package.json index fb09532b3..9547dee69 100644 --- a/plugins/fetch/package.json +++ b/plugins/fetch/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/find/package.json b/plugins/find/package.json index 03df7f724..ab83cab3c 100644 --- a/plugins/find/package.json +++ b/plugins/find/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/findAll/package.json b/plugins/findAll/package.json index bf2bbedb9..9819dab68 100644 --- a/plugins/findAll/package.json +++ b/plugins/findAll/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/insert/package.json b/plugins/insert/package.json index a852def6d..be8ae6629 100644 --- a/plugins/insert/package.json +++ b/plugins/insert/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/limit/package.json b/plugins/limit/package.json index 7c926757b..294ed3d9e 100644 --- a/plugins/limit/package.json +++ b/plugins/limit/package.json @@ -32,6 +32,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/order/package.json b/plugins/order/package.json index df98d67d8..c8ca3cb04 100644 --- a/plugins/order/package.json +++ b/plugins/order/package.json @@ -32,6 +32,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/permit-all/package.json b/plugins/permit-all/package.json index d0ca5a8b2..b56289639 100644 --- a/plugins/permit-all/package.json +++ b/plugins/permit-all/package.json @@ -32,6 +32,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/remove/package.json b/plugins/remove/package.json index b4243eddd..76f50e90a 100644 --- a/plugins/remove/package.json +++ b/plugins/remove/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/replace/package.json b/plugins/replace/package.json index ffbe1ec9a..75649752d 100644 --- a/plugins/replace/package.json +++ b/plugins/replace/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/store/package.json b/plugins/store/package.json index 6f76272ba..441c2b5a6 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json index 48dc6d99c..f96f9d191 100644 --- a/plugins/timeout/package.json +++ b/plugins/timeout/package.json @@ -32,6 +32,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/update/package.json b/plugins/update/package.json index 241097255..f59ed998f 100644 --- a/plugins/update/package.json +++ b/plugins/update/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/upsert/package.json b/plugins/upsert/package.json index 9303403aa..df9bddf22 100644 --- a/plugins/upsert/package.json +++ b/plugins/upsert/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } diff --git a/plugins/watch/package.json b/plugins/watch/package.json index 0f5b95eb3..9d2fce4ac 100644 --- a/plugins/watch/package.json +++ b/plugins/watch/package.json @@ -33,6 +33,7 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0" } } From 3f42c9de0e0cb9c43e5e2f9bf980885bde7ce772 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Mon, 5 Sep 2016 00:59:19 -0700 Subject: [PATCH 67/86] migrating server tests to integration tests --- babelify.sh | 2 +- plugins/permissions/src/test/validator.js | 32 + server/test/permissions.js | 616 ------------------- server/test/schema.js | 616 ------------------- server/test/test.js | 39 -- test/.babelrc | 3 + test/.eslintrc.js | 2 +- test/package.json | 7 +- test/{ => src}/serve.js | 0 test/src/test/permissions.js | 335 ++++++++++ {server => test/src}/test/prereq_tests.js | 0 {server => test/src}/test/protocol_tests.js | 0 {server => test/src}/test/query_tests.js | 0 test/src/test/schema.js | 99 +++ {server => test/src}/test/subscribe_tests.js | 0 test/src/test/test.js | 38 ++ {server => test/src}/test/utils.js | 41 +- {server => test/src}/test/write_tests.js | 6 +- 18 files changed, 541 insertions(+), 1295 deletions(-) create mode 100644 plugins/permissions/src/test/validator.js delete mode 100644 server/test/permissions.js delete mode 100644 server/test/schema.js delete mode 100644 server/test/test.js create mode 100644 test/.babelrc rename test/{ => src}/serve.js (100%) create mode 100644 test/src/test/permissions.js rename {server => test/src}/test/prereq_tests.js (100%) rename {server => test/src}/test/protocol_tests.js (100%) rename {server => test/src}/test/query_tests.js (100%) create mode 100644 test/src/test/schema.js rename {server => test/src}/test/subscribe_tests.js (100%) create mode 100644 test/src/test/test.js rename {server => test/src}/test/utils.js (91%) rename {server => test/src}/test/write_tests.js (99%) diff --git a/babelify.sh b/babelify.sh index c58e73d3e..fca3b41cc 100755 --- a/babelify.sh +++ b/babelify.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -dirs="server plugins plugin-router plugin-utils" +dirs="server plugins plugin-router plugin-utils test" for path in `find $dirs -name .babelrc | grep -v node_modules`; do { babel ${path%%.babelrc}/src -d ${path%%.babelrc}/dist -s true -w diff --git a/plugins/permissions/src/test/validator.js b/plugins/permissions/src/test/validator.js new file mode 100644 index 000000000..456a50af3 --- /dev/null +++ b/plugins/permissions/src/test/validator.js @@ -0,0 +1,32 @@ +// RSI: link this up +describe('Validator', () => { + it('unparseable', () => { + assert.throws(() => new Validator('() => ;'), /Unexpected token/); + }); + + it('broken', () => { + const validator = new Validator('() => foo'); + assert.throws(() => validator.is_valid(), /Validation error/); + }); + + it('permitted', () => { + const validator = new Validator(permitted_validator); + assert(validator.is_valid({ id: 3 })); + assert(validator.is_valid({ id: 3 }, { id: 0 })); + assert(validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 })); + }); + + it('user permitted', () => { + const validator = new Validator(user_permitted_validator); + assert(validator.is_valid({ id: 3 }, { id: 3 })); + assert(validator.is_valid({ id: 3 }, { id: 13 })); + assert(!validator.is_valid({ id: 3 }, { id: 4 })); + }); + + it('forbidden', () => { + const validator = new Validator(forbidden_validator); + assert(!validator.is_valid({ id: 3 })); + assert(!validator.is_valid({ id: 3 }, { id: 3 })); + assert(!validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 })); + }); +}); diff --git a/server/test/permissions.js b/server/test/permissions.js deleted file mode 100644 index a3ffbadc9..000000000 --- a/server/test/permissions.js +++ /dev/null @@ -1,616 +0,0 @@ -'use strict'; - -const hz_rule = require('../src/permissions/rule'); -const hz_validator = require('../src/permissions/validator'); -const query = require('../src/endpoint/query'); -const subscribe = require('../src/endpoint/subscribe'); -const insert = require('../src/endpoint/insert'); -const store = require('../src/endpoint/store'); -const update = require('../src/endpoint/update'); -const upsert = require('../src/endpoint/upsert'); -const replace = require('../src/endpoint/replace'); -const remove = require('../src/endpoint/remove'); -const utils = require('./utils'); - -const assert = require('assert'); - -const r = require('rethinkdb'); - -const Rule = hz_rule.Rule; -const Ruleset = hz_rule.Ruleset; -const Validator = hz_validator.Validator; - -const make_request = (type, collection, options) => { - if (collection !== null) { - return {request_id: 5, type, options: Object.assign({collection}, options)}; - } else { - return {request_id: 5, type, options}; - } -}; - -const context = {id: 3, groups: ['admin', 'default', 'authenticated']}; - -// Permit all rows -const permitted_validator = ` -(context) => { - if (!context) { throw new Error('no context'); } - return true; -} -`; - -// Forbid all rows -const forbidden_validator = ` -(context) => { - if (!context) { throw new Error('no context'); } - return false; -} -`; - -// Permit a row when the user's id is the last digit of the row's id -const user_permitted_validator = ` -(context, a, b) => { - if (!context) { throw new Error('no context'); } - const value = (a && a.id) || (b && b.id); - return context.id === (value % 10); -} -`; - -const all_tests = (collection) => { - describe('Template', () => { - it('any', () => { - const rule = new Rule('foo', {template: 'any()'}); - - const tests = [{ }, - {type: 'query', options: {collection: 'test'}}, - {fake: 'bar'}, - {options: { }}, - {type: 'query', options: {fake: 'baz'}}]; - - for (const t of tests) { - assert(rule.is_match(t, context)); - assert(rule.is_valid()); - } - }); - - it('any read', () => { - const rule = new Rule('foo', {template: 'collection(any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('fake', 'test', { }), context)); - assert(!rule.is_match(make_request('store', 'test', { }), context)); - assert(!rule.is_match(make_request('query', null, { }), context)); - assert(rule.is_match(make_request('query', 'fake', { }), context)); - assert(rule.is_match(make_request('query', 'fake', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }, { }]}), context)); - assert(!rule.is_match(make_request('subscribe', null, { }), context)); - assert(rule.is_match(make_request('subscribe', 'fake', { }), context)); - assert(rule.is_match(make_request('subscribe', 'fake', {find: { }}), context)); - assert(rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(rule.is_match(make_request('subscribe', 'test', {find_all: [{ }, { }]}), context)); - }); - - it('any read with collection', () => { - const rule = new Rule('foo', {template: 'collection("test").anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { }), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - }); - - it('any read with order', () => { - // TODO: allow for any number of fields in order - const rule = new Rule('foo', {template: 'collection("test").order(any(), any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {order: ['foo', 'ascending']}), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(!rule.is_match(make_request('query', 'test', {order: ['baz']}), context)); - assert(!rule.is_match(make_request('query', 'test', {order: ['baz', 'fake']}), context)); - assert(!rule.is_match(make_request('query', 'test', {order: [['fake']]}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [['foo'], 'ascending']}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [['bar'], 'descending']}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [['baz'], 'fake']}), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); - assert(rule.is_match(make_request('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); - }); - - it('any read with find', () => { - const rule = new Rule('foo', {template: 'collection("test").find(any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {find: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }, fake: 'baz'}), context)); - }); - - it('any read with findAll', () => { - // TODO: allow for any number of arguments in findAll - const rule = new Rule('foo', {template: 'collection("test").findAll(any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {find_all: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); - }); - - it('single key in findAll', () => { - const rule = new Rule('foo', {template: 'collection("test").findAll({ owner: userId() }).fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: (context.id + 1) } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id }, { other: context.id } ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id } ] }), context)); - }); - - it('multiple keys in findAll', () => { - const rule = new Rule('foo', {template: 'collection("test").findAll({ owner: userId(), key: any() }).fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: (context.id + 1) } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, bar: 'baz' } ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { owner: context.id, key: 3 } ] }), context)); - }); - - it('multiple items in findAll', () => { - const rule = new Rule('foo', { template: 'collection("test").findAll({ a: userId() }, { b: userId() })' }); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', { find_all: { } }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: true }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: (context.id + 1) }, { b: context.id } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id, bar: 'baz' } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id, b: context.id } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { b: context.id } ] }), context)); - assert(!rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id }, { b: context.id, bar: 'baz' } ] }), context)); - assert(rule.is_match(make_request('query', 'test', { find_all: [ { a: context.id }, { b: context.id } ] }), context)); - }); - - it('collection fetch', () => { - const rule = new Rule('foo', {template: 'collection("test").fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { }), context)); - assert(!rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {id: 5}}), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - }); - - it('collection watch', () => { - const rule = new Rule('foo', {template: 'collection("test").watch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('subscribe', 'fake', { }), context)); - assert(!rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(!rule.is_match(make_request('subscribe', 'test', {find: {id: 5}}), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - }); - - for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { - it(`collection ${type}`, () => { - const rule = new Rule('foo', {template: `collection("test").${type}(any())`}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request(type, 'test', { }), context)); - assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: []}), context)); - assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); - }); - it(`collection ${type} batch`, () => { - const rule = new Rule('foo', {template: `collection("test").${type}(anyArray(any()))`}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request(type, 'test', { }), context)); - assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); - assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: []}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); - }); - } - - it('any write', () => { - const rule = new Rule('foo', {template: 'collection("test").anyWrite()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('fake', 'test', { }), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(!rule.is_match(make_request('store', null, { }), context)); - - for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { - assert(!rule.is_match(make_request(type, 'fake', { }), context)); - assert(rule.is_match(make_request(type, 'test', {data: []}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [], bar: 'baz'}), context)); - } - }); - - it('userId in find', () => { - const rule = new Rule('foo', {template: 'collection("test").find({ owner: userId() }).fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: []}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); - assert(rule.is_match(make_request('query', 'test', {find: {owner: context.id}}), context)); - }); - - it('adds readAny() implicitly', () => { - { - const rule = new Rule('foo', {template: 'collection("test")'}); - assert(rule.is_valid()); - assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); - } - { - const rule = new Rule('foo', {template: 'collection("test").find({bar: any()})'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); - } - }); - - it('error on incomplete template', () => { - assert.throws(() => new Rule('foo', {template: '({ })'}), /Incomplete template/); - assert.throws(() => new Rule('foo', {template: '[ ]'}), /Invalid template/); - assert.throws(() => new Rule('foo', {template: '5'}), /Invalid template/); - assert.throws(() => new Rule('foo', {template: 'null'}), /Invalid template/); - }); - }); - - describe('Validator', () => { - it('unparseable', () => { - assert.throws(() => new Validator('() => ;'), /Unexpected token/); - }); - - it('broken', () => { - const validator = new Validator('() => foo'); - assert.throws(() => validator.is_valid(), /Validation error/); - }); - - it('permitted', () => { - const validator = new Validator(permitted_validator); - assert(validator.is_valid({ id: 3 })); - assert(validator.is_valid({ id: 3 }, { id: 0 })); - assert(validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 })); - }); - - it('user permitted', () => { - const validator = new Validator(user_permitted_validator); - assert(validator.is_valid({ id: 3 }, { id: 3 })); - assert(validator.is_valid({ id: 3 }, { id: 13 })); - assert(!validator.is_valid({ id: 3 }, { id: 4 })); - }); - - it('forbidden', () => { - const validator = new Validator(forbidden_validator); - assert(!validator.is_valid({ id: 3 })); - assert(!validator.is_valid({ id: 3 }, { id: 3 })); - assert(!validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 })); - }); - }); - - describe('Validation', () => { - const metadata = { - collection: () => ({ - table: r.table(collection), - get_matching_index: () => ({ name: 'id', fields: [ 'id' ] }), - }), - connection: () => utils.rdb_conn(), - }; - - const table_data = [ ]; - for (let i = 0; i < 10; ++i) { - table_data.push({ id: i }); - } - - beforeEach('Clear test table', () => - r.table(collection).delete().run(utils.rdb_conn())); - beforeEach('Populate test table', () => - r.table(collection).insert(table_data).run(utils.rdb_conn())); - - const make_run = (run_fn) => - (options, validator, limit) => new Promise((resolve, reject) => { - let cancel_fn; - const request = { options }; - const results = [ ]; - const ruleset = new Ruleset(); - ruleset.update([ new Rule('test', { template: 'any()', validator }) ]); - options.collection = collection; - - const add_response = (res) => { - res.data.forEach((item) => results.push(item)); - if (limit && results.length >= limit) { - cancel_fn(); - resolve(results); - } - }; - - cancel_fn = run_fn( - request, { id: 3 }, ruleset, metadata, add_response, - (res_or_error) => { - if (res_or_error instanceof Error) { - res_or_error.results = results; - reject(res_or_error); - } else { - if (res_or_error) { - add_response(res_or_error); - } - resolve(results); - } - }); - }); - - describe('query', () => { - const run = make_run(query.run); - it('permitted', () => - run({ order: [ [ 'id' ], 'ascending' ] }, permitted_validator).then((res) => { - assert.deepStrictEqual(res, table_data); - })); - - it('half-permitted', () => - run({ order: [ [ 'id' ], 'ascending' ], above: [ { id: 3 }, 'closed' ] }, user_permitted_validator).then(() => { - assert(false, 'Read should not have been permitted.'); - }).catch((err) => { - assert.strictEqual(err.message, 'Operation not permitted.'); - // Check that we got the permitted row or nothing (race condition) - if (err.results.length !== 0) { - assert.deepStrictEqual(err.results, [ { id: 3 } ]); - } - })); - - it('forbidden', () => - run({ }, forbidden_validator).then(() => { - assert(false, 'Read should not have been permitted.'); - }).catch((err) => { - assert.strictEqual(err.message, 'Operation not permitted.'); - assert.strictEqual(err.results.length, 0); - })); - }); - - describe('subscribe', () => { - const run = make_run(subscribe.run); - it('permitted with subsequent permitted change', () => { - // TODO: can't use run, need to issue a write during the subscription - }); - - it('permitted with subsequent forbidden change', () => { - // TODO: can't use run, need to issue a write during the subscription - }); - - it('half-permitted', () => - run({ order: [ [ 'id' ], 'ascending' ], above: [ { id: 3 }, 'closed' ] }, user_permitted_validator).then(() => { - assert(false, 'Read should not have been permitted.'); - }).catch((err) => { - assert.strictEqual(err.message, 'Operation not permitted.'); - // Check that we got the permitted row or nothing (race condition) - if (err.results.length !== 0) { - assert.deepStrictEqual(err.results, [ { id: 3 } ]); - } - })); - - it('forbidden', () => - run({ }, forbidden_validator).then(() => { - assert(false, 'Read should not have been permitted.'); - }).catch((err) => { - assert.strictEqual(err.message, 'Operation not permitted.'); - assert.strictEqual(err.results.length, 0); - })); - }); - - describe('insert', () => { - const run = make_run(insert.run); - it('permitted', () => - run({ data: [ { id: 11 } ] }, permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 11); - return r.table(collection).get(11).eq(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('permitted based on context', () => - run({ data: [ { id: 13 } ] }, user_permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 13); - return r.table(collection).get(13).eq(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('forbidden', () => - run({ data: [ { id: 11 } ] }, forbidden_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(11).ne(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - - it('forbidden based on context', () => - run({ data: [ { id: 11 } ] }, user_permitted_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(11).ne(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - }); - - describe('store', () => { - const run = make_run(store.run); - it('permitted', () => - run({ data: [ { id: 11 } ] }, permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 11); - return r.table(collection).get(11).eq(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('permitted based on context', () => - run({ data: [ { id: 13 } ] }, user_permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 13); - return r.table(collection).get(13).eq(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('forbidden', () => - run({ data: [ { id: 11 } ] }, forbidden_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(11).ne(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - - it('forbidden based on context', () => - run({ data: [ { id: 11 } ] }, user_permitted_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(11).ne(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - }); - - describe('upsert', () => { - const run = make_run(upsert.run); - it('permitted', () => - run({ data: [ { id: 11 } ] }, permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 11); - return r.table(collection).get(11).eq(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('permitted based on context', () => - run({ data: [ { id: 13 } ] }, user_permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 13); - return r.table(collection).get(13).eq(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('forbidden', () => - run({ data: [ { id: 11 } ] }, forbidden_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(11).ne(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - - it('forbidden based on context', () => - run({ data: [ { id: 11 } ] }, user_permitted_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(11).ne(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - }); - - describe('update', () => { - const run = make_run(update.run); - it('permitted', () => - run({ data: [ { id: 1, value: 5 } ] }, permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 1); - return r.table(collection).get(1).hasFields('value').not() - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('permitted based on context', () => - run({ data: [ { id: 3, value: 5 } ] }, user_permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 3); - return r.table(collection).get(3).hasFields('value').not() - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('forbidden', () => - run({ data: [ { id: 1, value: 5 } ] }, forbidden_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(1).hasFields('value') - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - - it('forbidden based on context', () => - run({ data: [ { id: 1, value: 5 } ] }, user_permitted_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(1).hasFields('value') - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - }); - - describe('replace', () => { - const run = make_run(replace.run); - it('permitted', () => - run({ data: [ { id: 1, value: 5 } ] }, permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 1); - return r.table(collection).get(1).hasFields('value').not() - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('permitted based on context', () => - run({ data: [ { id: 3, value: 5 } ] }, user_permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 3); - return r.table(collection).get(3).hasFields('value').not() - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('forbidden', () => - run({ data: [ { id: 1, value: 5 } ] }, forbidden_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(1).hasFields('value') - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - - it('forbidden based on context', () => - run({ data: [ { id: 1, value: 5 } ] }, user_permitted_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(1).hasFields('value') - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - }); - - describe('remove', () => { - const run = make_run(remove.run); - it('permitted', () => - run({ data: [ { id: 1 } ] }, permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 1); - return r.table(collection).get(1).ne(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('permitted based on context', () => - run({ data: [ { id: 3 } ] }, user_permitted_validator).then((res) => { - assert.strictEqual(res.length, 1); - assert.strictEqual(res[0].id, 3); - return r.table(collection).get(3).ne(null) - .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); - })); - - it('forbidden', () => - run({ data: [ { id: 1 } ] }, forbidden_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(1).eq(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - - it('forbidden based on context', () => - run({ data: [ { id: 1 } ] }, user_permitted_validator).then((res) => { - assert.deepStrictEqual(res, [ { error: 'Operation not permitted.' } ]); - return r.table(collection).get(1).eq(null) - .branch(r.error('write went through'), null).run(utils.rdb_conn()); - })); - }); - }); -}; - -const suite = (collection) => describe('Permissions', () => all_tests(collection)); - -module.exports = { suite }; diff --git a/server/test/schema.js b/server/test/schema.js deleted file mode 100644 index 1991f5849..000000000 --- a/server/test/schema.js +++ /dev/null @@ -1,616 +0,0 @@ -'use strict'; - -const horizon_protocol = require('../schema/horizon_protocol'); -const utils = require('./utils'); - -const assert = require('assert'); - -describe('Schema', () => { - const test_required_fields = (schema, valid, fields) => { - fields.forEach((f) => { - const request = Object.assign({}, valid); // Create a shallow copy - request[f] = undefined; - const error = schema.validate(request).error; - utils.check_error(error, `"${f}" is required`); - }); - }; - - const test_extra_field = (schema, valid) => { - const request = Object.assign({}, valid); // Create a shallow copy - request.fake_field = false; - const error = schema.validate(request).error; - utils.check_error(error, '"fake_field" is not allowed'); - }; - - describe('Protocol', () => { - describe('Request', () => { - const valid = { - request_id: 1, - type: 'query', - options: { }, - }; - - it('valid', () => { - const parsed = horizon_protocol.request.validate(valid); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, valid); - }); - - it('required fields', () => { - test_required_fields(horizon_protocol.request, valid, - ['request_id', 'type', 'options']); - }); - - it('wrong "request_id" type', () => { - const request = Object.assign({}, valid); - request.request_id = 'str'; - const error = horizon_protocol.request.validate(request).error; - utils.check_error(error, '"request_id" must be a number'); - }); - - it('wrong "type" type', () => { - const request = Object.assign({}, valid); - request.type = 5; - const error = horizon_protocol.request.validate(request).error; - utils.check_error(error, '"type" must be a string'); - }); - - it('wrong "options" type', () => { - const request = Object.assign({}, valid); - request.options = [5, 6]; - const error = horizon_protocol.request.validate(request).error; - utils.check_error(error, '"options" must be an object'); - }); - - it('extra field', () => { - test_extra_field(horizon_protocol.request, valid); - }); - }); - - describe('Write', () => { - const write_without_id = { - collection: 'horizon', - data: [{field: 4}], - }; - - const write_with_id = { - collection: 'horizon', - data: [{id: 5, field: 4}], - }; - - // In order to reduce the number of tests, these were written assuming - // that only two types of write schemas exist: id-required and id-optional. - // If this ever changes, this test will fail and more tests may need to - // be added. - it('common write schemas', () => { - // These schemas do not require an id in each "data" object - assert.equal(horizon_protocol.insert, horizon_protocol.upsert); - assert.equal(horizon_protocol.insert, horizon_protocol.store); - - // These schemas require an id in each "data" object - assert.equal(horizon_protocol.replace, horizon_protocol.update); - assert.equal(horizon_protocol.replace, horizon_protocol.remove); - }); - - describe('Insert', () => { - it('with id', () => { - const error = horizon_protocol.insert.validate(write_with_id).error; - assert.ifError(error); - }); - - it('without id', () => { - const error = horizon_protocol.insert.validate(write_without_id).error; - assert.ifError(error); - }); - - it('required fields', () => { - test_required_fields(horizon_protocol.insert, write_with_id, - ['collection', 'data']); - }); - - it('extra field', () => { - test_extra_field(horizon_protocol.insert, write_with_id); - }); - - it('wrong "collection" type', () => { - const request = Object.assign({}, write_with_id); - request.collection = true; - const error = horizon_protocol.insert.validate(request).error; - utils.check_error(error, '"collection" must be a string'); - }); - - it('wrong "collection" value', () => { - const request = Object.assign({}, write_with_id); - request.collection = '*.*'; - const error = horizon_protocol.insert.validate(request).error; - utils.check_error(error, '"collection" must only contain alpha-numeric and underscore characters'); - }); - - it('wrong "data" type', () => { - const request = Object.assign({}, write_with_id); - request.data = 'abc'; - const error = horizon_protocol.insert.validate(request).error; - utils.check_error(error, '"data" must be an array'); - }); - - it('wrong "data" member type', () => { - const request = Object.assign({}, write_with_id); - request.data = [7]; - const error = horizon_protocol.insert.validate(request).error; - utils.check_error(error, '"0" must be an object'); - }); - - it('empty "data" array', () => { - const request = Object.assign({}, write_with_id); - request.data = []; - const error = horizon_protocol.insert.validate(request).error; - utils.check_error(error, '"data" must contain at least 1 items'); - }); - }); - - describe('Replace', () => { - it('with id', () => { - const error = horizon_protocol.replace.validate(write_with_id).error; - assert.ifError(error); - }); - - it('without id', () => { - const error = horizon_protocol.replace.validate(write_without_id).error; - utils.check_error(error, '"id" is required'); - }); - - it('required fields', () => { - test_required_fields(horizon_protocol.replace, write_with_id, - ['collection', 'data']); - }); - - it('extra field', () => { - test_extra_field(horizon_protocol.replace, write_with_id); - }); - - it('wrong "collection" type', () => { - const request = Object.assign({}, write_with_id); - request.collection = true; - const error = horizon_protocol.replace.validate(request).error; - utils.check_error(error, '"collection" must be a string'); - }); - - it('wrong "collection" value', () => { - const request = Object.assign({}, write_with_id); - request.collection = '*.*'; - const error = horizon_protocol.insert.validate(request).error; - utils.check_error(error, '"collection" must only contain alpha-numeric and underscore characters'); - }); - - it('wrong "data" type', () => { - const request = Object.assign({}, write_with_id); - request.data = 'abc'; - const error = horizon_protocol.replace.validate(request).error; - utils.check_error(error, '"data" must be an array'); - }); - - it('wrong "data" member type', () => { - const request = Object.assign({}, write_with_id); - request.data = [7]; - const error = horizon_protocol.replace.validate(request).error; - utils.check_error(error, '"0" must be an object'); - }); - - it('empty "data" array', () => { - const request = Object.assign({}, write_with_id); - request.data = []; - const error = horizon_protocol.replace.validate(request).error; - utils.check_error(error, '"data" must contain at least 1 items'); - }); - }); - }); - - describe('Read', () => { - // The 'query' and 'subscribe' requests use the same schema - it('common read schemas', () => { - assert.equal(horizon_protocol.query, horizon_protocol.subscribe); - }); - - describe('no selection', () => { - const valid = { - collection: 'horizon', - }; - - it('valid', () => { - const parsed = horizon_protocol.query.validate(valid); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, valid); - }); - - it('required fields', () => { - test_required_fields(horizon_protocol.query, valid, - ['collection']); - }); - - it('extra field', () => { - test_extra_field(horizon_protocol.query, valid); - }); - - it('order', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('above', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - request.above = [{id: 10}, 'open']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('below', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - request.below = [{id: 5}, 'open']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('limit', () => { - const request = Object.assign({}, valid); - request.limit = 2; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('above and below and limit', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - request.below = [{id: 0}, 'closed']; - request.below = [{id: 5}, 'closed']; - request.limit = 4; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('wrong "collection" type', () => { - const request = Object.assign({}, valid); - request.collection = null; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"collection" must be a string'); - }); - - it('wrong "collection" value', () => { - const request = Object.assign({}, valid); - request.collection = '*.*'; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"collection" must only contain alpha-numeric and underscore characters'); - }); - - it('wrong "order" type', () => { - const request = Object.assign({}, valid); - request.order = true; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" must be an array'); - }); - - it('wrong "order" value', () => { - const request = Object.assign({}, valid); - { - request.order = []; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" does not contain [fields, direction]'); - } { - request.order = [['id']]; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" does not contain [direction]'); - } { - request.order = [{ }, 'ascending']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"fields" must be an array'); - } { - request.order = [[], 'descending']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"fields" must contain at least 1 item'); - } { - request.order = [['field'], 'baleeted']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"direction" must be one of [ascending, descending]'); - } - }); - - it('"above" without "order"', () => { - const request = Object.assign({}, valid); - request.above = [{id: 5}, 'open']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('wrong "above" type', () => { - const request = Object.assign({}, valid); - request.above = true; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"above" must be an array'); - }); - - it('wrong "above" value', () => { - const request = Object.assign({}, valid); - { - request.above = []; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"above" does not contain [value, bound_type]'); - } { - request.above = [1, 'closed']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"value" must be an object'); - } { - request.above = [{ }, 'open']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"value" must have 1 child'); - } { - request.above = [{id: 4}, 5]; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"bound_type" must be a string'); - } { - request.above = [{id: 3}, 'ajar']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"bound_type" must be one of [open, closed]'); - } - }); - - it('"below" without "order"', () => { - const request = Object.assign({}, valid); - request.below = [{id: 1}, 'open']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('wrong "below" type', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - request.below = true; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"below" must be an array'); - }); - - it('wrong "below" value', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - { - request.below = []; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"below" does not contain [value, bound_type]'); - } { - request.below = [1, 'closed']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"value" must be an object'); - } { - request.below = [{ }, 'open']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"value" must have 1 child'); - } { - request.below = [{id: 4}, 5]; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"bound_type" must be a string'); - } { - request.below = [{id: 3}, 'ajar']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"bound_type" must be one of [open, closed]'); - } - }); - - it('wrong "limit" type', () => { - const request = Object.assign({}, valid); - request.limit = true; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"limit" must be a number'); - }); - - it('wrong "limit" value', () => { - const request = Object.assign({}, valid); - { - request.limit = -1; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"limit" must be greater than -1'); - } { - request.limit = 1.5; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"limit" must be an integer'); - } - }); - }); - - describe('find', () => { - const valid = { - collection: 'horizon', - find: {score: 4}, - }; - - it('valid', () => { - const parsed = horizon_protocol.query.validate(valid); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, valid); - }); - - it('order', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" is not allowed'); - }); - - it('above', () => { - const request = Object.assign({}, valid); - request.above = [{id: 3}, 'open']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"above" is not allowed'); - }); - - it('below', () => { - const request = Object.assign({}, valid); - request.below = [{id: 4}, 'closed']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"below" is not allowed'); - }); - - it('limit', () => { - const request = Object.assign({}, valid); - request.limit = 4; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"limit" is not allowed'); - }); - - it('wrong "find" type', () => { - const request = Object.assign({}, valid); - request.find = 'score'; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"find" must be an object'); - }); - }); - - describe('find_all multiple', () => { - const valid = { - collection: 'horizon', - find_all: [{score: 2}, {score: 5, id: 0}], - }; - - it('valid', () => { - const parsed = horizon_protocol.query.validate(valid); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, valid); - }); - - it('order', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'descending']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" is not allowed'); - }); - - it('limit', () => { - const request = Object.assign({}, valid); - request.limit = 2; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('above', () => { - const request = Object.assign({}, valid); - { - request.above = [{id: 3}, 'closed']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"above" is not allowed'); - } { - request.order = [['id'], 'ascending']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" is not allowed'); - } - }); - - it('below', () => { - const request = Object.assign({}, valid); - { - request.below = [{id: 9}, 'closed']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"below" is not allowed'); - } { - request.order = [['id'], 'descending']; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"order" is not allowed'); - } - }); - }); - - describe('find_all one', () => { - const valid = { - collection: 'horizon', - find_all: [{score: 8, id: 5}], - }; - - it('valid', () => { - const parsed = horizon_protocol.query.validate(valid); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, valid); - }); - - it('order', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'descending']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('limit', () => { - const request = Object.assign({}, valid); - request.limit = 2; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('above', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'ascending']; - request.above = [{id: 3}, 'closed']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('below', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'descending']; - request.below = [{id: 9}, 'closed']; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('above and below and limit', () => { - const request = Object.assign({}, valid); - request.order = [['id'], 'descending']; - request.above = [{id: 'foo'}, 'open']; - request.below = [{id: 'bar'}, 'closed']; - request.limit = 59; - const parsed = horizon_protocol.query.validate(request); - assert.ifError(parsed.error); - assert.deepStrictEqual(parsed.value, request); - }); - - it('wrong "find_all" type', () => { - const request = Object.assign({}, valid); - request.find_all = null; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"find_all" must be an array'); - }); - - it('wrong "find_all" value', () => { - const request = Object.assign({}, valid); - { - request.find_all = []; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"find_all" must contain at least 1 items'); - } { - request.find_all = [{ }]; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"item" must have at least 1 child'); - } - }); - - it('with "find"', () => { - const request = Object.assign({}, valid); - request.find = {id: 7}; - const error = horizon_protocol.query.validate(request).error; - utils.check_error(error, '"find" is not allowed'); // TODO: better message? - }); - }); - }); - }); -}); diff --git a/server/test/test.js b/server/test/test.js deleted file mode 100644 index 80a9569ba..000000000 --- a/server/test/test.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -require('source-map-support').install(); - -const logger = require('../logger'); -const utils = require('./utils'); - -const all_suites = ['prereq_tests', - 'protocol_tests', - 'query_tests', - 'subscribe_tests', - 'write_tests', - 'permissions' ]; -const collection = 'test'; - -before('Start RethinkDB Server', () => utils.start_rethinkdb()); -after('Stop RethinkDB Server', () => utils.stop_rethinkdb()); - -beforeEach( - /** @this mocha */ - function() { logger.info(`Start test '${this.currentTest.title}'`); }); - -afterEach( - /** @this mocha */ - function() { logger.info(`End test '${this.currentTest.title}'`); }); - -describe('Horizon Server', - /** @this mocha */ - function() { - before('Start Horizon Server', utils.start_horizon_server); - after('Close Horizon Server', utils.close_horizon_server); - - before(`Creating general-purpose collection: '${collection}'`, - (done) => utils.create_collection(collection, done)); - - beforeEach('Connect Horizon Client', utils.open_horizon_conn); - afterEach('Close Horizon Client', utils.close_horizon_conn); - all_suites.forEach((s) => require(`./${s}`).suite(collection)); - }); diff --git a/test/.babelrc b/test/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/test/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/test/.eslintrc.js b/test/.eslintrc.js index bc66210fa..c1b0c17c6 100644 --- a/test/.eslintrc.js +++ b/test/.eslintrc.js @@ -5,7 +5,7 @@ const ERROR = 2; module.exports = { extends: "../.eslintrc.js", rules: { - "camelcase": [ ERROR ], +// "camelcase": [ ERROR ], "max-len": [ ERROR, 89 ], "prefer-template": [ OFF ], }, diff --git a/test/package.json b/test/package.json index 1bd77685f..65b31b682 100644 --- a/test/package.json +++ b/test/package.json @@ -5,7 +5,7 @@ "main": "dist/main.js", "scripts": { "lint": "eslint src test", - "test": "mocha test", + "test": "mocha dist/test", "build": "babel src -d dist -s true" }, "repository": { @@ -33,6 +33,9 @@ "istanbul": "^0.4.3", "mocha": "^2.5.3", "babel-cli": "^6.11.4", - "babel-preset-es2015": "^6.9.0" + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.0", + "rethinkdb": "^2.1.1", + "ws": "^1.1.0" } } diff --git a/test/serve.js b/test/src/serve.js similarity index 100% rename from test/serve.js rename to test/src/serve.js diff --git a/test/src/test/permissions.js b/test/src/test/permissions.js new file mode 100644 index 000000000..f0635bff9 --- /dev/null +++ b/test/src/test/permissions.js @@ -0,0 +1,335 @@ +'use strict'; + +const utils = require('./utils'); + +const assert = require('assert'); + +const r = require('rethinkdb'); + +const make_request = (type, collection, options) => { + if (collection !== null) { + return {request_id: 5, type, options: Object.assign({collection}, options)}; + } else { + return {request_id: 5, type, options}; + } +}; + +const user_id = 3; + +// Permit all rows +const permitted_validator = ` +(context) => { + if (!context) { throw new Error('no context'); } + return true; +} +`; + +// Forbid all rows +const forbidden_validator = ` +(context) => { + if (!context) { throw new Error('no context'); } + return false; +} +`; + +// Permit a row when the user's id is the last digit of the row's id +const user_permitted_validator = ` +(context, a, b) => { + if (!context) { throw new Error('no context'); } + const value = (a && a.user.id) || (b && b.user.id); + return context.id === (value % 10); +} +`; + +const all_tests = (collection) => { + describe('Validation', () => { + const metadata = { + collection: () => ({ + table: r.table(collection), + get_matching_index: () => ({name: 'id', fields: ['id']}), + }), + connection: () => utils.rdb_conn(), + }; + + const table_data = []; + for (let i = 0; i < 10; ++i) { + table_data.push({id: i}); + } + + beforeEach('Clear test table', () => + r.table(collection).delete().run(utils.rdb_conn())); + beforeEach('Populate test table', () => + r.table(collection).insert(table_data).run(utils.rdb_conn())); + before('Create user row', () => + r.table('users').insert(context).run(utils.rdb_conn())); + + const run = (options, validator) => new Promise((resolve, reject) => { + // Write group row into database + r.table('hz_groups').insert( + {id: 'default', rules: r.literal({test: {template: 'any()', validator}})}, + {conflict: 'update'}).run(utils.rdb_conn()); + + // Construct query and send on websocket + utils.stream_test({request_id: 1, options}, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }); + + describe('query', () => { + it('permitted', () => + run({order: [['id'], 'ascending'], query: []}, permitted_validator).then((res) => { + assert.deepStrictEqual(res, table_data); + })); + + it('half-permitted', () => + run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], query: []}, + user_permitted_validator).then(() => { + assert(false, 'Read should not have been permitted.'); + }).catch((err) => { + assert.strictEqual(err.message, 'Operation not permitted.'); + // Check that we got the permitted row or nothing (race condition) + if (err.results.length !== 0) { + assert.deepStrictEqual(err.results, [{id: 3}]); + } + })); + + it('forbidden', () => + run({query: []}, forbidden_validator).then(() => { + assert(false, 'Read should not have been permitted.'); + }).catch((err) => { + assert.strictEqual(err.message, 'Operation not permitted.'); + assert.strictEqual(err.results.length, 0); + })); + }); + + describe('subscribe', () => { + it('permitted with subsequent permitted change', () => { + // TODO: can't use run, need to issue a write during the subscription + }); + + it('permitted with subsequent forbidden change', () => { + // TODO: can't use run, need to issue a write during the subscription + }); + + it('half-permitted', () => + run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], subscribe: []}, + user_permitted_validator).then(() => { + assert(false, 'Read should not have been permitted.'); + }).catch((err) => { + assert.strictEqual(err.message, 'Operation not permitted.'); + // Check that we got the permitted row or nothing (race condition) + if (err.results.length !== 0) { + assert.deepStrictEqual(err.results, [{id: 3}]); + } + })); + + it('forbidden', () => + run({subscribe: []}, forbidden_validator).then(() => { + assert(false, 'Read should not have been permitted.'); + }).catch((err) => { + assert.strictEqual(err.message, 'Operation not permitted.'); + assert.strictEqual(err.results.length, 0); + })); + }); + + describe('insert', () => { + it('permitted', () => + run({insert: [{id: 11}]}, permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 11); + return r.table(collection).get(11).eq(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('permitted based on context', () => + run({insert: [{id: 13}]}, user_permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 13); + return r.table(collection).get(13).eq(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('forbidden', () => + run({insert: [{id: 11}]}, forbidden_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(11).ne(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + + it('forbidden based on context', () => + run({insert: [{id: 11}]}, user_permitted_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(11).ne(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + }); + + describe('store', () => { + it('permitted', () => + run({store: [{id: 11}]}, permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 11); + return r.table(collection).get(11).eq(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('permitted based on context', () => + run({store: [{id: 13}]}, user_permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 13); + return r.table(collection).get(13).eq(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('forbidden', () => + run({store: [{id: 11}]}, forbidden_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(11).ne(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + + it('forbidden based on context', () => + run({store: [{id: 11}]}, user_permitted_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(11).ne(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + }); + + describe('upsert', () => { + it('permitted', () => + run({upsert: [{id: 11}]}, permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 11); + return r.table(collection).get(11).eq(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('permitted based on context', () => + run({upsert: [{id: 13}]}, user_permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 13); + return r.table(collection).get(13).eq(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('forbidden', () => + run({upsert: [{id: 11}]}, forbidden_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(11).ne(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + + it('forbidden based on context', () => + run({upsert: [{id: 11}]}, user_permitted_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(11).ne(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + }); + + describe('update', () => { + it('permitted', () => + run({update: [{id: 1, value: 5}]}, permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 1); + return r.table(collection).get(1).hasFields('value').not() + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('permitted based on context', () => + run({update: [{id: 3, value: 5}]}, user_permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 3); + return r.table(collection).get(3).hasFields('value').not() + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('forbidden', () => + run({update: [{id: 1, value: 5}]}, forbidden_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(1).hasFields('value') + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + + it('forbidden based on context', () => + run({update: [{id: 1, value: 5}]}, user_permitted_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(1).hasFields('value') + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + }); + + describe('replace', () => { + it('permitted', () => + run({replace: [{id: 1, value: 5}]}, permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 1); + return r.table(collection).get(1).hasFields('value').not() + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('permitted based on context', () => + run({replace: [{id: 3, value: 5}]}, user_permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 3); + return r.table(collection).get(3).hasFields('value').not() + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('forbidden', () => + run({replace: [{id: 1, value: 5}]}, forbidden_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(1).hasFields('value') + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + + it('forbidden based on context', () => + run({replace: [{id: 1, value: 5}]}, user_permitted_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(1).hasFields('value') + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + }); + + describe('remove', () => { + it('permitted', () => + run({remove: [{id: 1}]}, permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 1); + return r.table(collection).get(1).ne(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('permitted based on context', () => + run({remove: [{id: 3}]}, user_permitted_validator).then((res) => { + assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].id, 3); + return r.table(collection).get(3).ne(null) + .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); + })); + + it('forbidden', () => + run({remove: [{id: 1}]}, forbidden_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(1).eq(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + + it('forbidden based on context', () => + run({remove: [{id: 1}]}, user_permitted_validator).then((res) => { + assert.deepStrictEqual(res, [{error: 'Operation not permitted.'}]); + return r.table(collection).get(1).eq(null) + .branch(r.error('write went through'), null).run(utils.rdb_conn()); + })); + }); + }); +}; + +const suite = (collection) => describe('Permissions', () => all_tests(collection)); + +module.exports = {suite}; diff --git a/server/test/prereq_tests.js b/test/src/test/prereq_tests.js similarity index 100% rename from server/test/prereq_tests.js rename to test/src/test/prereq_tests.js diff --git a/server/test/protocol_tests.js b/test/src/test/protocol_tests.js similarity index 100% rename from server/test/protocol_tests.js rename to test/src/test/protocol_tests.js diff --git a/server/test/query_tests.js b/test/src/test/query_tests.js similarity index 100% rename from server/test/query_tests.js rename to test/src/test/query_tests.js diff --git a/test/src/test/schema.js b/test/src/test/schema.js new file mode 100644 index 000000000..6c66c92be --- /dev/null +++ b/test/src/test/schema.js @@ -0,0 +1,99 @@ +'use strict'; + +require('source-map-support').install(); + +const {request: requestSchema} = require('@horizon/server/dist/schema/horizon_protocol'); +const utils = require('./utils'); + +const assert = require('assert'); + +describe('Schema', () => { + const test_required_fields = (schema, valid, fields) => { + fields.forEach((f) => { + const request = Object.assign({}, valid); // Create a shallow copy + request[f] = undefined; + const error = schema.validate(request).error; + utils.check_error(error, `"${f}" is required`); + }); + }; + + const test_extra_field = (schema, valid) => { + const request = Object.assign({}, valid); // Create a shallow copy + request.fake_field = false; + const error = schema.validate(request).error; + utils.check_error(error, '"fake_field" is not allowed'); + }; + + describe('Protocol', () => { + describe('Request', () => { + const valid = { + request_id: 1, + options: {query: []}, + }; + + it('allows options', () => { + const parsed = requestSchema.validate(valid); + assert.ifError(parsed.error); + assert.deepStrictEqual(parsed.value, valid); + }); + + it('allows keepalive', () => { + const request = Object.assign({type: 'keepalive'}, valid); + delete request.options; + const parsed = requestSchema.validate(request); + assert.ifError(parsed.error); + assert.deepStrictEqual(parsed.value, request); + }); + + it('rejects keepalive with options', () => { + const request = Object.assign({type: 'keepalive'}, valid); + const error = requestSchema.validate(request).error; + utils.check_error(error, '"options" is not allowed'); + }); + + it('allows end_subscription', () => { + const request = Object.assign({type: 'end_subscription'}, valid); + delete request.options; + const parsed = requestSchema.validate(request); + assert.ifError(parsed.error); + assert.deepStrictEqual(parsed.value, request); + }); + + it('rejects end_subscription with options', () => { + const request = Object.assign({type: 'end_subscription'}, valid); + const error = requestSchema.validate(request).error; + utils.check_error(error, '"options" is not allowed'); + }); + + it('requires fields', () => { + test_required_fields(requestSchema, valid, + ['request_id', 'options']); + }); + + it('rejects wrong "request_id" type', () => { + const request = Object.assign({}, valid); + request.request_id = 'str'; + const error = requestSchema.validate(request).error; + utils.check_error(error, '"request_id" must be a number'); + }); + + it('rejects wrong "type" value', () => { + const request = Object.assign({}, valid); + request.type = 5; + const error = requestSchema.validate(request).error; + utils.check_error(error, '"type" must be one of'); + }); + + it('rejects wrong "options" type', () => { + const request = Object.assign({}, valid); + request.options = [5, 6]; + const error = requestSchema.validate(request).error; + utils.check_error(error, '"options" must be an object'); + }); + + it('rejects unknown fields', () => { + test_extra_field(requestSchema, valid); + }); + }); + }); +}); diff --git a/server/test/subscribe_tests.js b/test/src/test/subscribe_tests.js similarity index 100% rename from server/test/subscribe_tests.js rename to test/src/test/subscribe_tests.js diff --git a/test/src/test/test.js b/test/src/test/test.js new file mode 100644 index 000000000..c14d18a24 --- /dev/null +++ b/test/src/test/test.js @@ -0,0 +1,38 @@ +'use strict'; + +require('source-map-support').install(); + +const utils = require('./utils'); + +const logger = require('@horizon/server').logger; + +const all_suites = ['prereq_tests', + 'protocol_tests', + 'query_tests', + 'subscribe_tests', + 'write_tests', + 'permissions']; +const collection = 'test'; + +before('Start RethinkDB Server', () => utils.start_rethinkdb()); +after('Stop RethinkDB Server', () => utils.stop_rethinkdb()); + +beforeEach( + /** @this mocha */ + function() { logger.info(`Start test '${this.currentTest.title}'`); }); + +afterEach( + /** @this mocha */ + function() { logger.info(`End test '${this.currentTest.title}'`); }); + +describe('Horizon Server', () => { + before('Start Horizon Server', utils.start_horizon_server); + after('Close Horizon Server', utils.close_horizon_server); + + before(`Creating general-purpose collection: '${collection}'`, + (done) => utils.create_collection(collection, done)); + + beforeEach('Connect Horizon Client', utils.open_horizon_conn); + afterEach('Close Horizon Client', utils.close_horizon_conn); + all_suites.forEach((s) => require(`./${s}`).suite(collection)); +}); diff --git a/server/test/utils.js b/test/src/test/utils.js similarity index 91% rename from server/test/utils.js rename to test/src/test/utils.js index 5e787fc3e..949de17e7 100644 --- a/server/test/utils.js +++ b/test/src/test/utils.js @@ -1,18 +1,20 @@ 'use strict'; -const horizon = require('../server'); -const logger = require('../logger'); +const horizon = require('@horizon/server'); +const PluginRouter = require('@horizon/plugin-router'); +const defaults = require('@horizon-plugins/defaults'); +const logger = horizon.logger; -const rm_sync_recursive = require('../../../cli/src/utils/rm_sync_recursive'); -const start_rdb_server = require('../../../cli/src/utils/start_rdb_server'); -const each_line_in_pipe = require('../../../cli/src/utils/each_line_in_pipe'); +const rm_sync_recursive = require('horizon/src/utils/rm_sync_recursive'); +const start_rdb_server = require('horizon/src/utils/start_rdb_server'); +const each_line_in_pipe = require('horizon/src/utils/each_line_in_pipe'); const assert = require('assert'); const http = require('http'); const r = require('rethinkdb'); const websocket = require('ws'); -const project_name = 'unittest'; +const project_name = 'integration_test'; const data_dir = './rethinkdb_data_test'; const log_file = `./horizon_test_${process.pid}.log`; @@ -125,17 +127,22 @@ const start_horizon_server = (done) => { http_server.listen(0, () => { logger.info('creating horizon server'); horizon_port = http_server.address().port; - horizon_server = new horizon.Server(http_server, - {project_name, - rdb_port, - auto_create_collection: true, - auto_create_index: true, - permissions: true, - auth: { - token_secret: 'hunter2', - allow_unauthenticated: true, - }, - }); + horizon_server = new horizon.Server(http_server, { + project_name, + rdb_port, + auth: { + token_secret: 'hunter2', + allow_unauthenticated: true, + }, + }); + + const plugin_router = new PluginRouter(horizon_server); + plugin_router.add(defaults({ + auto_create_collection: true, + auto_create_index: true, + })); + + horizon_server.set_middleware(plugin_router.hzMiddleware()); horizon_server.on('ready', () => { logger.info('horizon server ready'); diff --git a/server/test/write_tests.js b/test/src/test/write_tests.js similarity index 99% rename from server/test/write_tests.js rename to test/src/test/write_tests.js index a2d216c23..af2c18790 100644 --- a/server/test/write_tests.js +++ b/test/src/test/write_tests.js @@ -1,13 +1,13 @@ 'use strict'; const utils = require('./utils'); -const horizon_writes = require('../endpoint/writes'); +const pluginUtils = require('@horizon/plugin-utils'); const assert = require('assert'); const crypto = require('crypto'); -const hz_v = horizon_writes.version_field; -const invalidated_msg = horizon_writes.invalidated_msg; +const hz_v = pluginUtils.version_field; +const invalidated_msg = pluginUtils.writes.invalidated_msg; // Before each test, ids [0, 4) will be present in the collection const original_data = [ From d6b196ab7f9f5b530df9e41d166031ff372eca6a Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 6 Sep 2016 17:18:14 -0700 Subject: [PATCH 68/86] fix a few test bugs --- plugins/permissions/src/permissions.js | 2 +- server/package.json | 2 +- server/src/index.js | 1 + test/src/test/{query_tests.js => fetch.js} | 51 +++++++------ test/src/test/permissions.js | 15 ++-- test/src/test/{prereq_tests.js => prereq.js} | 9 ++- .../test/{protocol_tests.js => protocol.js} | 38 ++++++---- test/src/test/test.js | 12 ++-- test/src/test/utils.js | 71 ++++++++++--------- .../src/test/{subscribe_tests.js => watch.js} | 4 +- test/src/test/{write_tests.js => write.js} | 15 ++-- 11 files changed, 115 insertions(+), 105 deletions(-) rename test/src/test/{query_tests.js => fetch.js} (92%) rename test/src/test/{prereq_tests.js => prereq.js} (92%) rename test/src/test/{protocol_tests.js => protocol.js} (70%) rename test/src/test/{subscribe_tests.js => watch.js} (61%) rename test/src/test/{write_tests.js => write.js} (96%) diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index ddfd281ff..397fe3eaa 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -337,7 +337,7 @@ module.exports = (raw_config) => { next(new Error('client connection not authenticated')); } else { req.clientCtx[userSub].getValidatePromise(req).then((validate) => { - req.validate = validate; + req.setParameter(validate); next(); }).catch(next); } diff --git a/server/package.json b/server/package.json index b73b592b4..f1d17485a 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "lint": "eslint src test", - "test": "mocha --compilers js:babel-register --require babel-polyfill test --timeout 10000", + "test": "mocha test --timeout 10000", "coverage": "istanbul cover _mocha test/test.js", "build": "babel src -d dist -s true" }, diff --git a/server/src/index.js b/server/src/index.js index 896b76a6c..fe130a1d9 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -18,6 +18,7 @@ module.exports.Server = server.Server; module.exports.r = require('rethinkdb'); module.exports.logger = require('./logger'); +module.exports.protocol = server.protocol; const reliable = require('./reliable'); module.exports.Reliable = reliable.Reliable; diff --git a/test/src/test/query_tests.js b/test/src/test/fetch.js similarity index 92% rename from test/src/test/query_tests.js rename to test/src/test/fetch.js index 6a091fb46..3a70849cd 100644 --- a/test/src/test/query_tests.js +++ b/test/src/test/fetch.js @@ -8,16 +8,15 @@ const assert = require('assert'); const all_tests = (collection) => { const num_rows = 10; - before('Clear collection', (done) => utils.clear_collection(collection, done)); - before('Populate collection', (done) => utils.populate_collection(collection, num_rows, done)); + before('Clear collection', () => utils.clear_collection(collection)); + before('Populate collection', () => utils.populate_collection(collection, num_rows)); beforeEach('Authenticate client', utils.horizon_admin_auth); it('collection scan.', (done) => { utils.stream_test( { request_id: 0, - type: 'query', - options: {collection}, + options: {collection, query: []}, }, (err, res) => { assert.ifError(err); @@ -30,10 +29,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, order: [['id'], 'ascending'], + query: [], }, }, (err, res) => { @@ -47,10 +46,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, limit: 2, + query: [], }, }, (err, res) => { @@ -64,11 +63,11 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, order: [['id'], 'descending'], limit: 4, + query: [], }, }, (err, res) => { @@ -82,10 +81,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, above: [{id: 5}, 'closed'], + query: [], }, }, (err, res) => { @@ -99,10 +98,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, below: [{id: 5}, 'closed'], + query: [], }, }, (err, res) => { @@ -116,11 +115,11 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, above: [{id: 5}, 'open'], below: [{id: 7}, 'open'], + query: [], }, }, (err, res) => { @@ -134,10 +133,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find: {id: 4}, + query: [], }, }, (err, res) => { @@ -151,10 +150,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find: {id: 14}, + query: [], }, }, (err, res) => { @@ -168,10 +167,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{id: 4}, {id: 6}, {id: 9}], + query: [], }, }, (err, res) => { @@ -185,11 +184,11 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{id: 1}], order: [['value'], 'descending'], + query: [], }, }, (err, res) => { @@ -203,11 +202,11 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{id: 4}, {id: 8}, {id: 2}, {id: 1}], limit: 3, + query: [], }, }, (err, res) => { @@ -221,12 +220,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{id: 4}], order: [['value'], 'descending'], limit: 3, + query: [], }, }, (err, res) => { @@ -240,11 +239,11 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 1}], above: [{id: 3}, 'open'], + query: [], }, }, (err, res) => { @@ -258,11 +257,11 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 1}], below: [{id: 5}, 'open'], + query: [], }, }, (err, res) => { @@ -276,12 +275,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 1}], above: [{id: 1}, 'closed'], below: [{id: 9}, 'open'], + query: [], }, }, (err, res) => { @@ -295,12 +294,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 1}], order: [['id'], 'ascending'], above: [{id: 7}, 'open'], + query: [], }, }, (err, res) => { @@ -314,12 +313,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 0}], order: [['id'], 'descending'], below: [{id: 8}, 'open'], + query: [], }, }, (err, res) => { @@ -333,13 +332,13 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 0}], order: [['id'], 'descending'], above: [{id: 3}, 'closed'], below: [{id: 9}, 'closed'], + query: [], }, }, (err, res) => { @@ -355,12 +354,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{b: 4}, 'closed'], + query: [], }, }, (err) => { @@ -373,12 +372,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{a: 4}, 'closed'], + query: [], }, }, (err) => { @@ -391,12 +390,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{b: 4}, 'closed'], + query: [], }, }, (err) => { @@ -409,12 +408,12 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - type: 'query', options: { collection, find_all: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{a: 4}, 'closed'], + query: [], }, }, (err) => { diff --git a/test/src/test/permissions.js b/test/src/test/permissions.js index f0635bff9..be716f602 100644 --- a/test/src/test/permissions.js +++ b/test/src/test/permissions.js @@ -6,15 +6,8 @@ const assert = require('assert'); const r = require('rethinkdb'); -const make_request = (type, collection, options) => { - if (collection !== null) { - return {request_id: 5, type, options: Object.assign({collection}, options)}; - } else { - return {request_id: 5, type, options}; - } -}; - const user_id = 3; +const context = { user: { id: user_id } }; // Permit all rows const permitted_validator = ` @@ -106,7 +99,7 @@ const all_tests = (collection) => { })); }); - describe('subscribe', () => { + describe('watch', () => { it('permitted with subsequent permitted change', () => { // TODO: can't use run, need to issue a write during the subscription }); @@ -116,7 +109,7 @@ const all_tests = (collection) => { }); it('half-permitted', () => - run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], subscribe: []}, + run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], watch: []}, user_permitted_validator).then(() => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { @@ -128,7 +121,7 @@ const all_tests = (collection) => { })); it('forbidden', () => - run({subscribe: []}, forbidden_validator).then(() => { + run({watch: []}, forbidden_validator).then(() => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { assert.strictEqual(err.message, 'Operation not permitted.'); diff --git a/test/src/test/prereq_tests.js b/test/src/test/prereq.js similarity index 92% rename from test/src/test/prereq_tests.js rename to test/src/test/prereq.js index 81553f6db..c6ee27a0a 100644 --- a/test/src/test/prereq_tests.js +++ b/test/src/test/prereq.js @@ -6,7 +6,7 @@ const assert = require('assert'); const crypto = require('crypto'); const all_tests = (collection) => { - beforeEach('clear collection', (done) => utils.clear_collection(collection, done)); + beforeEach('clear collection', () => utils.clear_collection(collection)); beforeEach('authenticate', (done) => utils.horizon_admin_auth(done)); // Launch simultaneous queries that depend on a non-existent collection, then @@ -20,7 +20,7 @@ const all_tests = (collection) => { let finished = 0; for (let i = 0; i < query_count; ++i) { utils.stream_test( - {request_id: i, type: 'query', options: {collection: rand_collection}}, + {request_id: i, options: {collection: rand_collection, query: []}}, (err, res) => { assert.ifError(err); assert.strictEqual(res.length, 0); @@ -46,10 +46,9 @@ const all_tests = (collection) => { utils.stream_test( { request_id: i, - type: 'insert', options: { collection: rand_collection, - data: [{ }], + insert: [{ }], }, }, (err, res) => { @@ -77,10 +76,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: i, - type: 'query', options: { collection, order: [[field_name], 'ascending'], + query: [], }, }, (err, res) => { diff --git a/test/src/test/protocol_tests.js b/test/src/test/protocol.js similarity index 70% rename from test/src/test/protocol_tests.js rename to test/src/test/protocol.js index 09eaec786..34bf34073 100644 --- a/test/src/test/protocol_tests.js +++ b/test/src/test/protocol.js @@ -24,31 +24,43 @@ const all_tests = (collection) => { conn.send('{ }'); conn.once('close', (code, reason) => { assert.strictEqual(code, 1002); - assert(/^Protocol error: ValidationError/.test(reason)); + assert(/^Protocol error: Request validation error/.test(reason)); done(); }); }); - it('no type', (done) => { - utils.stream_test({request_id: 0}, (err, res) => { + it('keepalive', (done) => { + utils.stream_test({request_id: 0, type: 'keepalive'}, (err, res) => { assert.deepStrictEqual(res, []); - utils.check_error(err, '"type" is required'); + assert.ifError(err); + done(); + }); + }); + + it('end_subscription', (done) => { + const conn = utils.horizon_conn(); + conn.send('{"request_id": 0, "type": "end_subscription"}'); + + // There is no response for an end_subscription, so just run a dummy keepalive roundtrip + utils.stream_test({request_id: 0, type: 'keepalive'}, (err, res) => { + assert.deepStrictEqual(res, []); + assert.ifError(err); done(); }); }); it('no options', (done) => { - utils.stream_test({request_id: 1, type: 'fake'}, (err, res) => { + utils.stream_test({request_id: 1}, (err, res) => { assert.deepStrictEqual(res, []); utils.check_error(err, '"options" is required'); done(); }); }); - it('invalid endpoint', (done) => { - utils.stream_test({request_id: 2, type: 'fake', options: { }}, (err, res) => { + it('invalid method', (done) => { + utils.stream_test({request_id: 2, options: {fake: []}}, (err, res) => { assert.deepStrictEqual(res, []); - assert.strictEqual(err.message, '"fake" is not a registered request type.'); + assert.strictEqual(err.message, 'Error: no terminal in request'); done(); }); }); @@ -61,8 +73,10 @@ const all_tests = (collection) => { utils.horizon_conn().send(JSON.stringify( { request_id: 3, - type: 'subscribe', - options: {collection}, + options: { + collection: [collection], + watch: [], + }, })); utils.add_horizon_listener(3, (msg) => { if (msg.error !== undefined) { @@ -82,10 +96,10 @@ const all_tests = (collection) => { utils.horizon_conn().send(JSON.stringify( { request_id: 4, - type: 'query', options: { - collection, + collection: [collection], field_name: 'id', + query: [], }, }), () => (utils.close_horizon_conn(), done())); }); diff --git a/test/src/test/test.js b/test/src/test/test.js index c14d18a24..8a7651859 100644 --- a/test/src/test/test.js +++ b/test/src/test/test.js @@ -6,11 +6,11 @@ const utils = require('./utils'); const logger = require('@horizon/server').logger; -const all_suites = ['prereq_tests', - 'protocol_tests', - 'query_tests', - 'subscribe_tests', - 'write_tests', +const all_suites = ['prereq', + 'protocol', + 'fetch', + 'watch', + 'write', 'permissions']; const collection = 'test'; @@ -30,7 +30,7 @@ describe('Horizon Server', () => { after('Close Horizon Server', utils.close_horizon_server); before(`Creating general-purpose collection: '${collection}'`, - (done) => utils.create_collection(collection, done)); + () => utils.create_collection(collection)); beforeEach('Connect Horizon Client', utils.open_horizon_conn); afterEach('Close Horizon Client', utils.close_horizon_conn); diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 949de17e7..c81412b9f 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -67,55 +67,60 @@ const make_admin_token = () => { }; // Creates a collection, no-op if it already exists, uses horizon server prereqs -const create_collection = (collection, done) => { - assert.notStrictEqual(horizon_server, undefined); - assert.notStrictEqual(horizon_port, undefined); - const conn = new websocket(`ws://localhost:${horizon_port}/horizon`, - horizon.protocol, - {rejectUnauthorized: false}) - .once('error', (err) => assert.ifError(err)) - .on('open', () => { - conn.send(JSON.stringify({request_id: 123, method: 'token', token: make_admin_token()})); - conn.once('message', (data) => { - const res = JSON.parse(data); - assert.strictEqual(res.request_id, 123); - assert.strictEqual(typeof res.token, 'string'); - assert.strictEqual(res.id, 'admin'); - assert.strictEqual(res.provider, null); - - // This query should auto-create the collection if it's missing - conn.send(JSON.stringify({ - request_id: 0, - type: 'query', - options: {collection, limit: 0}, - })); - - conn.once('message', () => { - conn.close(); - done(); +function create_collection(collection) { + return new Promise((resolve, reject) => { + assert.notStrictEqual(horizon_server, undefined); + assert.notStrictEqual(horizon_port, undefined); + const conn = new websocket(`ws://localhost:${horizon_port}/horizon`, + horizon.protocol, + {rejectUnauthorized: false}) + .once('error', (err) => assert.ifError(err)) + .on('open', () => { + conn.send(JSON.stringify({request_id: 123, method: 'token', token: make_admin_token()})); + conn.once('message', (data) => { + const res = JSON.parse(data); + assert.strictEqual(res.request_id, 123); + assert.strictEqual(typeof res.token, 'string'); + assert.strictEqual(res.id, 'admin'); + assert.strictEqual(res.provider, null); + + // This query should auto-create the collection if it's missing + conn.send(JSON.stringify({ + request_id: 0, + options: {collection, limit: 0, query: []}, + })); + + conn.once('message', (data) => { + conn.close(); + if (data.error) { + reject(new Error(data.error)); + } else { + resolve(); + } + }); }); }); - }); + }); }; // Removes all data from a collection - does not remove indexes -const clear_collection = (collection, done) => { +function clear_collection(collection) { assert.notStrictEqual(rdb_conn, undefined); - table(collection).delete().run(rdb_conn).then(() => done()); + return table(collection).wait().do(() => table(collection).delete()).run(rdb_conn); }; // Populates a collection with the given rows // If `rows` is a number, fill in data using all keys in [0, rows) -const populate_collection = (collection, rows, done) => { +const populate_collection = (collection, rows) => { assert.notStrictEqual(rdb_conn, undefined); if (rows.constructor.name !== 'Array') { - table(collection).insert( + return table(collection).insert( r.range(rows).map( (i) => ({id: i, value: i.mod(4)}) - )).run(rdb_conn).then(() => done()); + )).run(rdb_conn); } else { - table(collection).insert(rows).run(rdb_conn).then(() => done()); + return table(collection).insert(rows).run(rdb_conn); } }; diff --git a/test/src/test/subscribe_tests.js b/test/src/test/watch.js similarity index 61% rename from test/src/test/subscribe_tests.js rename to test/src/test/watch.js index e05427758..eb9d2429b 100644 --- a/test/src/test/subscribe_tests.js +++ b/test/src/test/watch.js @@ -5,8 +5,8 @@ const utils = require('./utils'); const all_tests = (collection) => { const num_rows = 10; - before('Clear collection', (done) => utils.clear_collection(collection, done)); - before('Populate collection', (done) => utils.populate_collection(collection, num_rows, done)); + before('Clear collection', () => utils.clear_collection(collection)); + before('Populate collection', () => utils.populate_collection(collection, num_rows)); beforeEach('Authenticate client', utils.horizon_admin_auth); }; diff --git a/test/src/test/write_tests.js b/test/src/test/write.js similarity index 96% rename from test/src/test/write_tests.js rename to test/src/test/write.js index af2c18790..01b54a311 100644 --- a/test/src/test/write_tests.js +++ b/test/src/test/write.js @@ -48,8 +48,7 @@ const all_tests = (collection) => { const make_request = (type, data, options) => ({ request_id: crypto.randomBytes(4).readUInt32BE(), - type, - options: Object.assign({}, options || {}, {collection, data}), + options: Object.assign({type: []}, options || {}, {collection, data}), }); const check_collection = (expected, done) => { @@ -81,11 +80,11 @@ const all_tests = (collection) => { combine_sort_data(old_data, new_data, () => null, (row, map) => map.set(row.id, row)); - beforeEach('Clear collection', (done) => utils.clear_collection(collection, done)); + beforeEach('Clear collection', () => utils.clear_collection(collection)); describe('Basic writes', () => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); - beforeEach('Populate collection', (done) => utils.populate_collection(collection, original_data, done)); + beforeEach('Populate collection', () => utils.populate_collection(collection, original_data)); const request_from_ids = (type, ids) => make_request(type, ids.map(new_row_from_id)); @@ -213,7 +212,7 @@ const all_tests = (collection) => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); const test_data = [{id: 'versioned', [hz_v]: 11, foo: 'bar'}]; - beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); + beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); describe('Store', () => { const request = (row) => make_request('store', [row]); @@ -330,7 +329,7 @@ const all_tests = (collection) => { beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); const test_data = [{id: 'versionless', foo: 'bar'}]; - beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); + beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); describe('Store', () => { const request = (row) => make_request('store', [row]); @@ -496,7 +495,7 @@ const all_tests = (collection) => { describe('Existing Row', () => { const test_data = [{id: 0, value: 0}]; - beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); + beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); it('Store', (done) => { utils.stream_test(make_request('store', writes), (err, res) => { @@ -574,7 +573,7 @@ const all_tests = (collection) => { describe('Zero Timeout', () => { const timeout = {timeout: 0}; const test_data = [{id: 0, value: 0}]; - beforeEach('Populate collection', (done) => utils.populate_collection(collection, test_data, done)); + beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); it('Store', (done) => { utils.stream_test(make_request('store', writes, timeout), (err, res) => { From 9b1e023fb252ad55abd448e33b9015b4563c37e3 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 7 Sep 2016 16:58:18 -0700 Subject: [PATCH 69/86] added 5 sec wait for metadata to sync --- plugins/collection/src/collection.js | 21 +++++++++++++++++++-- plugins/collection/src/types/metadata.js | 4 ++-- test/src/test/protocol.js | 6 +++--- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 2bf0c47c2..0c6e052b2 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -12,8 +12,25 @@ function collection(metadata) { } else if (typeof args[0] !== 'string') { next(new Error('First argument to "collection" must be a string.')); } else { - metadata.collection(args[0]).then((c) => { - // RSI: pick up here trucks here reads aren't getting this? + new Promise((resolve, reject) => { + if (metadata.ready) { resolve(); } + + // Wait up to 5 seconds for metadata readiness + // This should only happen if the connection to the database just recovered, + // or there is some invalid data in the metadata. + const subs = metadata.subscribe({ + onReady: () => { + subs.close(); + clearTimeout(timer); + resolve(); + }, + }); + + const timer = setTimeout(() => { + reject(new Error('Timed out waiting for metadata ' + + 'to sync with the database.')); + }, 5000); + }).then(() => metadata.collection(args[0])).then((c) => { req.setParameter(c); next(); }).catch(next); diff --git a/plugins/collection/src/types/metadata.js b/plugins/collection/src/types/metadata.js index 593f99d8c..a960d5bca 100644 --- a/plugins/collection/src/types/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -285,7 +285,7 @@ class ReliableMetadata extends Reliable { throw new Error(`Collection "${name}" is reserved for internal use ` + 'and cannot be used in requests.'); } else if (!this.ready) { - throw new Error('ReliableMetadata is not ready.'); + throw new Error('Metadata is not synced with the database.'); } const collection = this._collections.get(name); @@ -315,7 +315,7 @@ class ReliableMetadata extends Reliable { throw new Error(`Collection "${name}" is reserved for internal use ` + 'and cannot be used in requests.'); } else if (!this.ready) { - throw new Error('ReliableMetadata is not ready.'); + throw new Error('Metadata is not synced with the database.'); } else if (this._collections.get(name)) { throw new Error(`Collection "${name}" already exists.`); } diff --git a/test/src/test/protocol.js b/test/src/test/protocol.js index 34bf34073..be0a313fe 100644 --- a/test/src/test/protocol.js +++ b/test/src/test/protocol.js @@ -38,11 +38,11 @@ const all_tests = (collection) => { }); it('end_subscription', (done) => { - const conn = utils.horizon_conn(); - conn.send('{"request_id": 0, "type": "end_subscription"}'); + //const conn = utils.horizon_conn(); + //conn.send('{"request_id": 0, "type": "end_subscription"}'); // There is no response for an end_subscription, so just run a dummy keepalive roundtrip - utils.stream_test({request_id: 0, type: 'keepalive'}, (err, res) => { + utils.stream_test({request_id: 1, type: 'keepalive'}, (err, res) => { assert.deepStrictEqual(res, []); assert.ifError(err); done(); From b14fa0dc8eded6ca799c31e2df8a16cb55160d79 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 7 Sep 2016 18:04:07 -0700 Subject: [PATCH 70/86] got protocol tests passing --- plugin-router/src/index.js | 12 +++++------- plugins/collection/src/collection.js | 5 +---- plugins/permissions/src/permissions.js | 14 +++++++++++--- test/src/test/protocol.js | 14 ++++++++++++-- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/plugin-router/src/index.js b/plugin-router/src/index.js index 587a78a9d..ea8c42c34 100644 --- a/plugin-router/src/index.js +++ b/plugin-router/src/index.js @@ -61,7 +61,6 @@ class PluginRouter extends EventEmitter { } for (const m in active.methods) { - console.log(`adding plugin method: ${m}`); if (this.methods[m]) { throw new Error(`Method name conflict: "${m}"`); } @@ -136,11 +135,10 @@ class PluginRouter extends EventEmitter { for (const o in req.options) { const m = this.methods[o]; if (m) { - console.log(`method for request: ${o}, type: ${m.type}`); if (m.type === 'terminal') { if (terminalName !== null) { - next(new Error('multiple terminals in request: ' + - `${terminalName}, ${o}`)); + next(new Error('Multiple terminal methods in request: ' + + `"${terminalName}", "${o}"`)); } else { terminalName = o; } @@ -153,15 +151,15 @@ class PluginRouter extends EventEmitter { } } } else { - console.log(`no ${o} method for request`); + next(new Error(`No method to handle option "${o}".`)); } } } if (terminalName === null) { - next(new Error('no terminal in request')); + next(new Error('No terminal method was specified in the request.')); } else if (requirements[terminalName]) { - next(new Error('terminal ${terminalName} is also a requirement')); + next(new Error(`Terminal method "${terminalName}" is also a requirement.`)); } else { const ordering = this.requirementsOrdering(); const middlewareChain = Object.keys(requirements).sort( diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 0c6e052b2..d1ffdf86e 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -49,10 +49,7 @@ module.exports = (options) => { Boolean(options.auto_create_collection), Boolean(options.auto_create_index)); - ctx[metadata].subscribe({onReady: () => { - console.log('metadata ready'); - onReady(); - }, onUnready}); + ctx[metadata].subscribe({onReady, onUnready}); return { methods: { diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 397fe3eaa..8260d62ce 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -22,7 +22,15 @@ function addToMapSet(map, name, el) { set = new Set(); map.set(name, set); } - // RSI: This might not always be an error. + set.add(el); +} + +function addToMapSetUnique(map, name, el) { + let set = map.get(name); + if (!set) { + set = new Set(); + map.set(name, set); + } assert(!set.has(el), `addToMapSet: ${name} already has ${el}`); set.add(el); } @@ -91,9 +99,9 @@ class RuleMap { addGroupRule(group, ruleName, rule) { this.rulenameToRule.set(ruleName, rule); - addToMapSet(this.groupToRulenames, group, ruleName); + addToMapSetUnique(this.groupToRulenames, group, ruleName); getMapSet(this.groupToUsers, group).forEach((user) => { - addToMapSet(this.userToRulenames, user, ruleName); + addToMapSetUnique(this.userToRulenames, user, ruleName); this.userToRulesetSymbol.set(user, Symbol()); }); } diff --git a/test/src/test/protocol.js b/test/src/test/protocol.js index be0a313fe..719fb74e6 100644 --- a/test/src/test/protocol.js +++ b/test/src/test/protocol.js @@ -57,10 +57,20 @@ const all_tests = (collection) => { }); }); - it('invalid method', (done) => { + it('no terminal method', (done) => { + utils.stream_test({request_id: 2, options: {above: []}}, (err, res) => { + assert.deepStrictEqual(res, []); + assert.strictEqual(err.message, + 'Error: No terminal method was specified in the request.'); + done(); + }); + }); + + it('unknown method', (done) => { utils.stream_test({request_id: 2, options: {fake: []}}, (err, res) => { assert.deepStrictEqual(res, []); - assert.strictEqual(err.message, 'Error: no terminal in request'); + assert.strictEqual(err.message, + 'Error: No method to handle option "fake".'); done(); }); }); From 03673caf9926c298c2106a145f8fe6cd408e10aa Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 7 Sep 2016 18:59:57 -0700 Subject: [PATCH 71/86] fixed collections and permissions readiness --- plugins/collection/src/collection.js | 23 ++++++---- plugins/permissions/src/permissions.js | 40 +++++++++------- test/src/test/fetch.js | 63 ++++++++++++++------------ test/src/test/utils.js | 7 ++- 4 files changed, 74 insertions(+), 59 deletions(-) diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index d1ffdf86e..ccf3eb205 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -49,16 +49,19 @@ module.exports = (options) => { Boolean(options.auto_create_collection), Boolean(options.auto_create_index)); - ctx[metadata].subscribe({onReady, onUnready}); - - return { - methods: { - collection: { - type: 'option', - handler: collection(ctx[metadata]), - }, - }, - }; + return new Promise((resolve, reject) => { + ctx[metadata].subscribe({onUnready, onReady: () => { + resolve({ + methods: { + collection: { + type: 'option', + handler: collection(ctx[metadata]), + }, + }, + }); + onReady(); + }}); + }); }, deactivate: (ctx) => { if (ctx[metadata]) { diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 8260d62ce..fd4e46192 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -320,7 +320,7 @@ module.exports = (raw_config) => { return { name: 'hz_permissions', - activate(ctx) { + activate(ctx, onReady, onUnready) { logger.info('Activating permissions plugin.'); ctx[userCache] = new UserCache(config, ctx); @@ -336,23 +336,29 @@ module.exports = (raw_config) => { }; ctx.on('disconnect', disconnectCb); - return { - methods: { - hz_permissions: { - type: 'preReq', - handler: (req, res, next) => { - if (!req.clientCtx[userSub]) { - next(new Error('client connection not authenticated')); - } else { - req.clientCtx[userSub].getValidatePromise(req).then((validate) => { - req.setParameter(validate); - next(); - }).catch(next); - } + return new Promise((resolve, reject) => { + ctx[userCache].groupCfeed.subscribe({onUnready, onReady: () => { + resolve({ + methods: { + hz_permissions: { + type: 'preReq', + handler: (req, res, next) => { + if (!req.clientCtx[userSub]) { + next(new Error('client connection not authenticated')); + } else { + req.clientCtx[userSub].getValidatePromise(req).then((validate) => { + req.setParameter(validate); + next(); + }).catch(next); + } + }, + }, }, - }, - }, - }; + }); + onReady(); + }}); + }); + }, deactivate(ctx) { diff --git a/test/src/test/fetch.js b/test/src/test/fetch.js index 3a70849cd..0f30a9281 100644 --- a/test/src/test/fetch.js +++ b/test/src/test/fetch.js @@ -16,7 +16,10 @@ const all_tests = (collection) => { utils.stream_test( { request_id: 0, - options: {collection, query: []}, + options: { + collection: [collection], + query: [] + }, }, (err, res) => { assert.ifError(err); @@ -30,7 +33,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], order: [['id'], 'ascending'], query: [], }, @@ -47,8 +50,8 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, - limit: 2, + collection: [collection], + limit: [2], query: [], }, }, @@ -64,9 +67,9 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], order: [['id'], 'descending'], - limit: 4, + limit: [4], query: [], }, }, @@ -82,7 +85,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], above: [{id: 5}, 'closed'], query: [], }, @@ -99,7 +102,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], below: [{id: 5}, 'closed'], query: [], }, @@ -116,7 +119,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], above: [{id: 5}, 'open'], below: [{id: 7}, 'open'], query: [], @@ -134,8 +137,8 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, - find: {id: 4}, + collection: [collection], + find: [{id: 4}], query: [], }, }, @@ -151,8 +154,8 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, - find: {id: 14}, + collection: [collection], + find: [{id: 14}], query: [], }, }, @@ -168,7 +171,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{id: 4}, {id: 6}, {id: 9}], query: [], }, @@ -185,7 +188,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{id: 1}], order: [['value'], 'descending'], query: [], @@ -203,9 +206,9 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{id: 4}, {id: 8}, {id: 2}, {id: 1}], - limit: 3, + limit: [3], query: [], }, }, @@ -221,10 +224,10 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{id: 4}], order: [['value'], 'descending'], - limit: 3, + limit: [3], query: [], }, }, @@ -240,7 +243,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 1}], above: [{id: 3}, 'open'], query: [], @@ -258,7 +261,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 1}], below: [{id: 5}, 'open'], query: [], @@ -276,7 +279,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 1}], above: [{id: 1}, 'closed'], below: [{id: 9}, 'open'], @@ -295,7 +298,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 1}], order: [['id'], 'ascending'], above: [{id: 7}, 'open'], @@ -314,7 +317,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 0}], order: [['id'], 'descending'], below: [{id: 8}, 'open'], @@ -333,7 +336,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 0}], order: [['id'], 'descending'], above: [{id: 3}, 'closed'], @@ -355,7 +358,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{b: 4}, 'closed'], @@ -373,7 +376,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{a: 4}, 'closed'], @@ -391,7 +394,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{b: 4}, 'closed'], @@ -409,7 +412,7 @@ const all_tests = (collection) => { { request_id: 0, options: { - collection, + collection: [collection], find_all: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{a: 4}, 'closed'], @@ -423,6 +426,6 @@ const all_tests = (collection) => { }); }; -const suite = (collection) => describe('Query', () => all_tests(collection)); +const suite = (collection) => describe('Fetch', () => all_tests(collection)); module.exports = {suite}; diff --git a/test/src/test/utils.js b/test/src/test/utils.js index c81412b9f..23f9de375 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -142,7 +142,7 @@ const start_horizon_server = (done) => { }); const plugin_router = new PluginRouter(horizon_server); - plugin_router.add(defaults({ + const plugins_promise = plugin_router.add(defaults({ auto_create_collection: true, auto_create_index: true, })); @@ -151,7 +151,10 @@ const start_horizon_server = (done) => { horizon_server.on('ready', () => { logger.info('horizon server ready'); - done(); + plugins_promise.then(() => { + console.log('all plugins ready'); + done(); + }).catch(done); }); horizon_server.on('unready', (server, err) => { logger.info(`horizon server unready: ${err}`); From 890070f8c403b68b86ff508a64dba7ac7f17479e Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 7 Sep 2016 19:46:52 -0700 Subject: [PATCH 72/86] fixed test create_collection --- plugins/collection/src/collection.js | 34 ++++++++++---------- test/src/test/fetch.js | 46 ++++++++++++++-------------- test/src/test/utils.js | 12 +++----- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index ccf3eb205..2e66b42de 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -13,23 +13,25 @@ function collection(metadata) { next(new Error('First argument to "collection" must be a string.')); } else { new Promise((resolve, reject) => { - if (metadata.ready) { resolve(); } - - // Wait up to 5 seconds for metadata readiness - // This should only happen if the connection to the database just recovered, - // or there is some invalid data in the metadata. - const subs = metadata.subscribe({ - onReady: () => { - subs.close(); - clearTimeout(timer); - resolve(); - }, - }); + if (metadata.ready) { + resolve(); + } else { + // Wait up to 5 seconds for metadata readiness + // This should only happen if the connection to the database just recovered, + // or there is some invalid data in the metadata. + const subs = metadata.subscribe({ + onReady: () => { + subs.close(); + clearTimeout(timer); + resolve(); + }, + }); - const timer = setTimeout(() => { - reject(new Error('Timed out waiting for metadata ' + - 'to sync with the database.')); - }, 5000); + const timer = setTimeout(() => { + reject(new Error('Timed out waiting for metadata ' + + 'to sync with the database.')); + }, 5000); + } }).then(() => metadata.collection(args[0])).then((c) => { req.setParameter(c); next(); diff --git a/test/src/test/fetch.js b/test/src/test/fetch.js index 0f30a9281..f1d55ba14 100644 --- a/test/src/test/fetch.js +++ b/test/src/test/fetch.js @@ -18,7 +18,7 @@ const all_tests = (collection) => { request_id: 0, options: { collection: [collection], - query: [] + fetch: [] }, }, (err, res) => { @@ -35,7 +35,7 @@ const all_tests = (collection) => { options: { collection: [collection], order: [['id'], 'ascending'], - query: [], + fetch: [], }, }, (err, res) => { @@ -52,7 +52,7 @@ const all_tests = (collection) => { options: { collection: [collection], limit: [2], - query: [], + fetch: [], }, }, (err, res) => { @@ -70,7 +70,7 @@ const all_tests = (collection) => { collection: [collection], order: [['id'], 'descending'], limit: [4], - query: [], + fetch: [], }, }, (err, res) => { @@ -87,7 +87,7 @@ const all_tests = (collection) => { options: { collection: [collection], above: [{id: 5}, 'closed'], - query: [], + fetch: [], }, }, (err, res) => { @@ -104,7 +104,7 @@ const all_tests = (collection) => { options: { collection: [collection], below: [{id: 5}, 'closed'], - query: [], + fetch: [], }, }, (err, res) => { @@ -122,7 +122,7 @@ const all_tests = (collection) => { collection: [collection], above: [{id: 5}, 'open'], below: [{id: 7}, 'open'], - query: [], + fetch: [], }, }, (err, res) => { @@ -139,7 +139,7 @@ const all_tests = (collection) => { options: { collection: [collection], find: [{id: 4}], - query: [], + fetch: [], }, }, (err, res) => { @@ -156,7 +156,7 @@ const all_tests = (collection) => { options: { collection: [collection], find: [{id: 14}], - query: [], + fetch: [], }, }, (err, res) => { @@ -173,7 +173,7 @@ const all_tests = (collection) => { options: { collection: [collection], find_all: [{id: 4}, {id: 6}, {id: 9}], - query: [], + fetch: [], }, }, (err, res) => { @@ -191,7 +191,7 @@ const all_tests = (collection) => { collection: [collection], find_all: [{id: 1}], order: [['value'], 'descending'], - query: [], + fetch: [], }, }, (err, res) => { @@ -209,7 +209,7 @@ const all_tests = (collection) => { collection: [collection], find_all: [{id: 4}, {id: 8}, {id: 2}, {id: 1}], limit: [3], - query: [], + fetch: [], }, }, (err, res) => { @@ -228,7 +228,7 @@ const all_tests = (collection) => { find_all: [{id: 4}], order: [['value'], 'descending'], limit: [3], - query: [], + fetch: [], }, }, (err, res) => { @@ -246,7 +246,7 @@ const all_tests = (collection) => { collection: [collection], find_all: [{value: 1}], above: [{id: 3}, 'open'], - query: [], + fetch: [], }, }, (err, res) => { @@ -264,7 +264,7 @@ const all_tests = (collection) => { collection: [collection], find_all: [{value: 1}], below: [{id: 5}, 'open'], - query: [], + fetch: [], }, }, (err, res) => { @@ -283,7 +283,7 @@ const all_tests = (collection) => { find_all: [{value: 1}], above: [{id: 1}, 'closed'], below: [{id: 9}, 'open'], - query: [], + fetch: [], }, }, (err, res) => { @@ -302,7 +302,7 @@ const all_tests = (collection) => { find_all: [{value: 1}], order: [['id'], 'ascending'], above: [{id: 7}, 'open'], - query: [], + fetch: [], }, }, (err, res) => { @@ -321,7 +321,7 @@ const all_tests = (collection) => { find_all: [{value: 0}], order: [['id'], 'descending'], below: [{id: 8}, 'open'], - query: [], + fetch: [], }, }, (err, res) => { @@ -341,7 +341,7 @@ const all_tests = (collection) => { order: [['id'], 'descending'], above: [{id: 3}, 'closed'], below: [{id: 9}, 'closed'], - query: [], + fetch: [], }, }, (err, res) => { @@ -362,7 +362,7 @@ const all_tests = (collection) => { find_all: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{b: 4}, 'closed'], - query: [], + fetch: [], }, }, (err) => { @@ -380,7 +380,7 @@ const all_tests = (collection) => { find_all: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{a: 4}, 'closed'], - query: [], + fetch: [], }, }, (err) => { @@ -398,7 +398,7 @@ const all_tests = (collection) => { find_all: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{b: 4}, 'closed'], - query: [], + fetch: [], }, }, (err) => { @@ -416,7 +416,7 @@ const all_tests = (collection) => { find_all: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{a: 4}, 'closed'], - query: [], + fetch: [], }, }, (err) => { diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 23f9de375..7b644990a 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -87,13 +87,14 @@ function create_collection(collection) { // This query should auto-create the collection if it's missing conn.send(JSON.stringify({ request_id: 0, - options: {collection, limit: 0, query: []}, + options: {collection: [collection], limit: [0], fetch: []}, })); conn.once('message', (data) => { + const message = JSON.parse(data); conn.close(); - if (data.error) { - reject(new Error(data.error)); + if (message.error) { + reject(new Error(message.error)); } else { resolve(); } @@ -151,10 +152,7 @@ const start_horizon_server = (done) => { horizon_server.on('ready', () => { logger.info('horizon server ready'); - plugins_promise.then(() => { - console.log('all plugins ready'); - done(); - }).catch(done); + plugins_promise.then(done).catch(done); }); horizon_server.on('unready', (server, err) => { logger.info(`horizon server unready: ${err}`); From b4f788297f620f9c2592a46bd83bccbf757b5fb6 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Wed, 7 Sep 2016 21:30:54 -0700 Subject: [PATCH 73/86] fixed some tests --- plugins/permissions/src/permissions.js | 16 +- plugins/permissions/src/rule.js | 6 +- plugins/permissions/src/test/template.js | 244 +++++++++++----------- plugins/permissions/src/test/validator.js | 20 +- plugins/permissions/src/validator.js | 2 +- server/src/server.js | 2 +- test/src/test/fetch.js | 56 ++--- test/src/test/utils.js | 2 +- 8 files changed, 176 insertions(+), 172 deletions(-) diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index fd4e46192..6d7ffbbe7 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -68,11 +68,15 @@ class RuleMap { } forEachUserRule(user, cb) { - this.userToRulenames.forEach((rn) => { - const rule = this.rulenameToRule.get(rn); - assert(rule); - cb(rule); - }); + const ruleset = this.userToRulenames.get(user); + + if (ruleset) { + ruleset.forEach((rn) => { + const rule = this.rulenameToRule.get(rn); + assert(rule); + cb(rule); + }); + } } addUserGroup(user, group) { @@ -280,7 +284,7 @@ class UserCache { return (...args) => { try { for (const rule of ruleset) { - if (rule.is_valid(...args)) { + if (rule.isValid(...args)) { return rule; } } diff --git a/plugins/permissions/src/rule.js b/plugins/permissions/src/rule.js index 48c44f072..7e6399bcc 100644 --- a/plugins/permissions/src/rule.js +++ b/plugins/permissions/src/rule.js @@ -11,15 +11,15 @@ class Rule { } } - is_match(query, context) { + isMatch(query, context) { return this.template.is_match(query, context); } - is_valid(...args) { + isValid(...args) { if (!this.validator) { return true; } - return this.validator.is_valid(...args); + return this.validator.isValid(...args); } } diff --git a/plugins/permissions/src/test/template.js b/plugins/permissions/src/test/template.js index dc1e6905a..d7a15b501 100644 --- a/plugins/permissions/src/test/template.js +++ b/plugins/permissions/src/test/template.js @@ -28,199 +28,199 @@ describe('Template', () => { {type: 'query', options: {fake: 'baz'}}]; for (const t of tests) { - assert(rule.is_match(t, context)); - assert(rule.is_valid()); + assert(rule.isMatch(t, context)); + assert(rule.isValid()); } }); it('any read', () => { const rule = new Rule({template: 'collection(any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('fake', 'test', { }), context)); - assert(!rule.is_match(make_request('store', 'test', { }), context)); - assert(!rule.is_match(make_request('query', null, { }), context)); - assert(rule.is_match(make_request('query', 'fake', { }), context)); - assert(rule.is_match(make_request('query', 'fake', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }, { }]}), context)); - assert(!rule.is_match(make_request('subscribe', null, { }), context)); - assert(rule.is_match(make_request('subscribe', 'fake', { }), context)); - assert(rule.is_match(make_request('subscribe', 'fake', {find: { }}), context)); - assert(rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(rule.is_match(make_request('subscribe', 'test', {find_all: [{ }, { }]}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('fake', 'test', { }), context)); + assert(!rule.isMatch(make_request('store', 'test', { }), context)); + assert(!rule.isMatch(make_request('query', null, { }), context)); + assert(rule.isMatch(make_request('query', 'fake', { }), context)); + assert(rule.isMatch(make_request('query', 'fake', {find: { }}), context)); + assert(rule.isMatch(make_request('query', 'test', {bar: 'baz'}), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }, { }]}), context)); + assert(!rule.isMatch(make_request('subscribe', null, { }), context)); + assert(rule.isMatch(make_request('subscribe', 'fake', { }), context)); + assert(rule.isMatch(make_request('subscribe', 'fake', {find: { }}), context)); + assert(rule.isMatch(make_request('subscribe', 'test', {bar: 'baz'}), context)); + assert(rule.isMatch(make_request('subscribe', 'test', {find_all: [{ }, { }]}), context)); }); it('any read with collection', () => { const rule = new Rule({template: 'collection("test").anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { }), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'fake', { }), context)); + assert(rule.isMatch(make_request('query', 'test', { }), context)); + assert(rule.isMatch(make_request('query', 'test', { }), context)); + assert(rule.isMatch(make_request('query', 'test', { }), context)); + assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); + assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); + assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); + assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); }); it('any read with order', () => { // TODO: allow for any number of fields in order const rule = new Rule({template: 'collection("test").order(any(), any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {order: ['foo', 'ascending']}), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(!rule.is_match(make_request('query', 'test', {order: ['baz']}), context)); - assert(!rule.is_match(make_request('query', 'test', {order: ['baz', 'fake']}), context)); - assert(!rule.is_match(make_request('query', 'test', {order: [['fake']]}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [['foo'], 'ascending']}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [['bar'], 'descending']}), context)); - assert(rule.is_match(make_request('query', 'test', {order: [['baz'], 'fake']}), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); - assert(rule.is_match(make_request('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'fake', {order: ['foo', 'ascending']}), context)); + assert(!rule.isMatch(make_request('query', 'test', { }), context)); + assert(!rule.isMatch(make_request('query', 'test', {order: ['baz']}), context)); + assert(!rule.isMatch(make_request('query', 'test', {order: ['baz', 'fake']}), context)); + assert(!rule.isMatch(make_request('query', 'test', {order: [['fake']]}), context)); + assert(rule.isMatch(make_request('query', 'test', {order: [['foo'], 'ascending']}), context)); + assert(rule.isMatch(make_request('query', 'test', {order: [['bar'], 'descending']}), context)); + assert(rule.isMatch(make_request('query', 'test', {order: [['baz'], 'fake']}), context)); + assert(rule.isMatch(make_request('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); + assert(rule.isMatch(make_request('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); }); it('any read with find', () => { const rule = new Rule({template: 'collection("test").find(any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {find: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {find: { }, fake: 'baz'}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'fake', {find: { }}), context)); + assert(!rule.isMatch(make_request('query', 'test', { }), context)); + assert(rule.isMatch(make_request('query', 'test', {find: { }}), context)); + assert(rule.isMatch(make_request('query', 'test', {find: { }, fake: 'baz'}), context)); }); it('any read with findAll', () => { // TODO: allow for any number of arguments in findAll const rule = new Rule({template: 'collection("test").findAll(any()).anyRead()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', {find_all: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'fake', {find_all: { }}), context)); + assert(!rule.isMatch(make_request('query', 'test', { }), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }]}), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); }); it('single key in findAll', () => { const rule = new Rule({template: 'collection("test").findAll({ owner: userId() }).fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id}, {other: context.id}]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id}]}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id}, {other: context.id}]}), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id}]}), context)); }); it('multiple keys in findAll', () => { const rule = new Rule({template: 'collection("test").findAll({ owner: userId(), key: any() }).fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{owner: context.id, key: 3}]}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id, key: 3}]}), context)); }); it('multiple items in findAll', () => { const rule = new Rule({template: 'collection("test").findAll({ a: userId() }, { b: userId() })'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find_all: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: []}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: (context.id + 1)}, {b: context.id}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id, bar: 'baz'}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id, b: context.id}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{b: context.id}]}), context)); - assert(!rule.is_match(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id, bar: 'baz'}]}), context)); - assert(rule.is_match(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id}]}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'test', {find_all: { }}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: true}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: []}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: (context.id + 1)}, {b: context.id}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id, bar: 'baz'}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id, b: context.id}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{b: context.id}]}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id, bar: 'baz'}]}), context)); + assert(rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id}]}), context)); }); it('collection fetch', () => { const rule = new Rule({template: 'collection("test").fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'fake', { }), context)); - assert(!rule.is_match(make_request('query', 'test', {bar: 'baz'}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {id: 5}}), context)); - assert(rule.is_match(make_request('query', 'test', { }), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'fake', { }), context)); + assert(!rule.isMatch(make_request('query', 'test', {bar: 'baz'}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find: {id: 5}}), context)); + assert(rule.isMatch(make_request('query', 'test', { }), context)); }); it('collection watch', () => { const rule = new Rule({template: 'collection("test").watch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('subscribe', 'fake', { }), context)); - assert(!rule.is_match(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(!rule.is_match(make_request('subscribe', 'test', {find: {id: 5}}), context)); - assert(rule.is_match(make_request('subscribe', 'test', { }), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('subscribe', 'fake', { }), context)); + assert(!rule.isMatch(make_request('subscribe', 'test', {bar: 'baz'}), context)); + assert(!rule.isMatch(make_request('subscribe', 'test', {find: {id: 5}}), context)); + assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); }); for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { it(`collection ${type}`, () => { const rule = new Rule({template: `collection("test").${type}(any())`}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request(type, 'test', { }), context)); - assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: []}), context)); - assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request(type, 'test', { }), context)); + assert(!rule.isMatch(make_request(type, 'test', {data: { }}), context)); + assert(!rule.isMatch(make_request(type, 'test', {data: []}), context)); + assert(!rule.isMatch(make_request(type, 'fake', {data: [{ }]}), context)); + assert(!rule.isMatch(make_request(type, 'test', {data: [{ }], fake: 6}), context)); + assert(rule.isMatch(make_request(type, 'test', {data: [{ }]}), context)); }); it(`collection ${type} batch`, () => { const rule = new Rule({template: `collection("test").${type}(anyArray(any()))`}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request(type, 'test', { }), context)); - assert(!rule.is_match(make_request(type, 'test', {data: { }}), context)); - assert(!rule.is_match(make_request(type, 'test', {data: [{ }], fake: 6}), context)); - assert(!rule.is_match(make_request(type, 'fake', {data: [{ }]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: []}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request(type, 'test', { }), context)); + assert(!rule.isMatch(make_request(type, 'test', {data: { }}), context)); + assert(!rule.isMatch(make_request(type, 'test', {data: [{ }], fake: 6}), context)); + assert(!rule.isMatch(make_request(type, 'fake', {data: [{ }]}), context)); + assert(rule.isMatch(make_request(type, 'test', {data: []}), context)); + assert(rule.isMatch(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.isMatch(make_request(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); }); } it('any write', () => { const rule = new Rule({template: 'collection("test").anyWrite()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('fake', 'test', { }), context)); - assert(!rule.is_match(make_request('query', 'test', { }), context)); - assert(!rule.is_match(make_request('store', null, { }), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('fake', 'test', { }), context)); + assert(!rule.isMatch(make_request('query', 'test', { }), context)); + assert(!rule.isMatch(make_request('store', null, { }), context)); for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { - assert(!rule.is_match(make_request(type, 'fake', { }), context)); - assert(rule.is_match(make_request(type, 'test', {data: []}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [{ }]}), context)); - assert(rule.is_match(make_request(type, 'test', {data: [], bar: 'baz'}), context)); + assert(!rule.isMatch(make_request(type, 'fake', { }), context)); + assert(rule.isMatch(make_request(type, 'test', {data: []}), context)); + assert(rule.isMatch(make_request(type, 'test', {data: [{ }]}), context)); + assert(rule.isMatch(make_request(type, 'test', {data: [], bar: 'baz'}), context)); } }); it('userId in find', () => { const rule = new Rule({template: 'collection("test").find({ owner: userId() }).fetch()'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: true}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: []}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); - assert(!rule.is_match(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); - assert(rule.is_match(make_request('query', 'test', {find: {owner: context.id}}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'test', {find: { }}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find: true}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find: []}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); + assert(!rule.isMatch(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); + assert(rule.isMatch(make_request('query', 'test', {find: {owner: context.id}}), context)); }); it('adds readAny() implicitly', () => { { const rule = new Rule({template: 'collection("test")'}); - assert(rule.is_valid()); - assert(rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(rule.isValid()); + assert(rule.isMatch(make_request('query', 'test', {find: { }}), context)); + assert(rule.isMatch(make_request('query', 'test', {find: {bar: 'baz'}}), context)); } { const rule = new Rule({template: 'collection("test").find({bar: any()})'}); - assert(rule.is_valid()); - assert(!rule.is_match(make_request('query', 'test', {find: { }}), context)); - assert(rule.is_match(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(rule.isValid()); + assert(!rule.isMatch(make_request('query', 'test', {find: { }}), context)); + assert(rule.isMatch(make_request('query', 'test', {find: {bar: 'baz'}}), context)); } }); diff --git a/plugins/permissions/src/test/validator.js b/plugins/permissions/src/test/validator.js index 456a50af3..d0e329d64 100644 --- a/plugins/permissions/src/test/validator.js +++ b/plugins/permissions/src/test/validator.js @@ -6,27 +6,27 @@ describe('Validator', () => { it('broken', () => { const validator = new Validator('() => foo'); - assert.throws(() => validator.is_valid(), /Validation error/); + assert.throws(() => validator.isValid(), /Validation error/); }); it('permitted', () => { const validator = new Validator(permitted_validator); - assert(validator.is_valid({ id: 3 })); - assert(validator.is_valid({ id: 3 }, { id: 0 })); - assert(validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 })); + assert(validator.isValid({ id: 3 })); + assert(validator.isValid({ id: 3 }, { id: 0 })); + assert(validator.isValid({ id: 3 }, { id: 0 }, { id: 1 })); }); it('user permitted', () => { const validator = new Validator(user_permitted_validator); - assert(validator.is_valid({ id: 3 }, { id: 3 })); - assert(validator.is_valid({ id: 3 }, { id: 13 })); - assert(!validator.is_valid({ id: 3 }, { id: 4 })); + assert(validator.isValid({ id: 3 }, { id: 3 })); + assert(validator.isValid({ id: 3 }, { id: 13 })); + assert(!validator.isValid({ id: 3 }, { id: 4 })); }); it('forbidden', () => { const validator = new Validator(forbidden_validator); - assert(!validator.is_valid({ id: 3 })); - assert(!validator.is_valid({ id: 3 }, { id: 3 })); - assert(!validator.is_valid({ id: 3 }, { id: 0 }, { id: 1 })); + assert(!validator.isValid({ id: 3 })); + assert(!validator.isValid({ id: 3 }, { id: 3 })); + assert(!validator.isValid({ id: 3 }, { id: 0 }, { id: 1 })); }); }); diff --git a/plugins/permissions/src/validator.js b/plugins/permissions/src/validator.js index e9d54f788..159a57d99 100644 --- a/plugins/permissions/src/validator.js +++ b/plugins/permissions/src/validator.js @@ -15,7 +15,7 @@ class Validator { assert(typeof this._fn === 'function'); } - is_valid(...args) { + isValid(...args) { return this._fn(...args); } } diff --git a/server/src/server.js b/server/src/server.js index 97988265f..b8e2e7a60 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -11,7 +11,7 @@ const Joi = require('joi'); const websocket = require('ws'); const r = require('rethinkdb'); -const protocolName = 'rethinkdb-horizon-v0'; +const protocolName = 'rethinkdb-horizon-v1'; function handleProtocols(protocols, cb) { if (protocols.findIndex((x) => x === protocolName) !== -1) { diff --git a/test/src/test/fetch.js b/test/src/test/fetch.js index f1d55ba14..6067f9c83 100644 --- a/test/src/test/fetch.js +++ b/test/src/test/fetch.js @@ -166,13 +166,13 @@ const all_tests = (collection) => { }); }); - it('find_all.', (done) => { + it('findAll.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{id: 4}, {id: 6}, {id: 9}], + findAll: [{id: 4}, {id: 6}, {id: 9}], fetch: [], }, }, @@ -183,13 +183,13 @@ const all_tests = (collection) => { }); }); - it('find_all order.', (done) => { + it('findAll order.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{id: 1}], + findAll: [{id: 1}], order: [['value'], 'descending'], fetch: [], }, @@ -201,13 +201,13 @@ const all_tests = (collection) => { }); }); - it('find_all limit.', (done) => { + it('findAll limit.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{id: 4}, {id: 8}, {id: 2}, {id: 1}], + findAll: [{id: 4}, {id: 8}, {id: 2}, {id: 1}], limit: [3], fetch: [], }, @@ -219,13 +219,13 @@ const all_tests = (collection) => { }); }); - it('find_all order limit.', (done) => { + it('findAll order limit.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{id: 4}], + findAll: [{id: 4}], order: [['value'], 'descending'], limit: [3], fetch: [], @@ -238,13 +238,13 @@ const all_tests = (collection) => { }); }); - it('find_all above.', (done) => { + it('findAll above.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 1}], + findAll: [{value: 1}], above: [{id: 3}, 'open'], fetch: [], }, @@ -256,13 +256,13 @@ const all_tests = (collection) => { }); }); - it('find_all below.', (done) => { + it('findAll below.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 1}], + findAll: [{value: 1}], below: [{id: 5}, 'open'], fetch: [], }, @@ -274,13 +274,13 @@ const all_tests = (collection) => { }); }); - it('find_all above below.', (done) => { + it('findAll above below.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 1}], + findAll: [{value: 1}], above: [{id: 1}, 'closed'], below: [{id: 9}, 'open'], fetch: [], @@ -293,13 +293,13 @@ const all_tests = (collection) => { }); }); - it('find_all order above.', (done) => { + it('findAll order above.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 1}], + findAll: [{value: 1}], order: [['id'], 'ascending'], above: [{id: 7}, 'open'], fetch: [], @@ -312,13 +312,13 @@ const all_tests = (collection) => { }); }); - it('find_all order below.', (done) => { + it('findAll order below.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 0}], + findAll: [{value: 0}], order: [['id'], 'descending'], below: [{id: 8}, 'open'], fetch: [], @@ -331,13 +331,13 @@ const all_tests = (collection) => { }); }); - it('find_all order above below.', (done) => { + it('findAll order above below.', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 0}], + findAll: [{value: 0}], order: [['id'], 'descending'], above: [{id: 3}, 'closed'], below: [{id: 9}, 'closed'], @@ -353,13 +353,13 @@ const all_tests = (collection) => { // These tests are impossible to represent in the schema (as far as I can tell), // so the test for this functionality must be at the integration level. - it('find_all "above" field not in "order".', (done) => { + it('findAll "above" field not in "order".', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 0}], + findAll: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{b: 4}, 'closed'], fetch: [], @@ -371,13 +371,13 @@ const all_tests = (collection) => { }); }); - it('find_all "above" field not first in "order".', (done) => { + it('findAll "above" field not first in "order".', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 0}], + findAll: [{value: 0}], order: [['value', 'a'], 'descending'], above: [{a: 4}, 'closed'], fetch: [], @@ -389,13 +389,13 @@ const all_tests = (collection) => { }); }); - it('find_all "below" field not in "order".', (done) => { + it('findAll "below" field not in "order".', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 0}], + findAll: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{b: 4}, 'closed'], fetch: [], @@ -407,13 +407,13 @@ const all_tests = (collection) => { }); }); - it('find_all "below" field not first in "order".', (done) => { + it('findAll "below" field not first in "order".', (done) => { utils.stream_test( { request_id: 0, options: { collection: [collection], - find_all: [{value: 0}], + findAll: [{value: 0}], order: [['value', 'a'], 'descending'], below: [{a: 4}, 'closed'], fetch: [], diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 7b644990a..7370a4a55 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -115,7 +115,7 @@ function clear_collection(collection) { const populate_collection = (collection, rows) => { assert.notStrictEqual(rdb_conn, undefined); - if (rows.constructor.name !== 'Array') { + if (!Array.isArray(rows)) { return table(collection).insert( r.range(rows).map( (i) => ({id: i, value: i.mod(4)}) From 750b95e7c4623fd221af85870bb43393932ed539 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 8 Sep 2016 01:21:48 -0700 Subject: [PATCH 74/86] reworking plugin-router, added stubs for differend backend implementations --- .../.babelrc | 0 .../.eslintrc.js | 0 .../package.json | 8 +- .../src/index.js | 18 ++- plugin-router-express/.babelrc | 3 + plugin-router-express/.eslintrc.js | 16 +++ plugin-router-express/package.json | 37 +++++ plugin-router-express/src/index.js | 27 ++++ .../src/request.js | 0 plugin-router-hapi/.babelrc | 3 + plugin-router-hapi/.eslintrc.js | 16 +++ plugin-router-hapi/package.json | 37 +++++ plugin-router-hapi/src/index.js | 27 ++++ plugin-router-hapi/src/request.js | 25 ++++ plugin-router-http/.babelrc | 3 + plugin-router-http/.eslintrc.js | 16 +++ plugin-router-http/package.json | 36 +++++ plugin-router-http/src/index.js | 26 ++++ plugin-router-http/src/request.js | 25 ++++ plugin-router-koa/.babelrc | 3 + plugin-router-koa/.eslintrc.js | 16 +++ plugin-router-koa/package.json | 37 +++++ plugin-router-koa/src/index.js | 26 ++++ plugin-router-koa/src/request.js | 25 ++++ server/src/client.js | 13 +- server/src/schema/server_options.js | 8 +- server/src/server.js | 135 ++++++++++++++++-- test/package.json | 2 +- test/src/serve.js | 6 +- test/src/test/utils.js | 8 +- 30 files changed, 570 insertions(+), 32 deletions(-) rename {plugin-router => plugin-router-base}/.babelrc (100%) rename {plugin-router => plugin-router-base}/.eslintrc.js (100%) rename {plugin-router => plugin-router-base}/package.json (78%) rename {plugin-router => plugin-router-base}/src/index.js (92%) create mode 100644 plugin-router-express/.babelrc create mode 100644 plugin-router-express/.eslintrc.js create mode 100644 plugin-router-express/package.json create mode 100644 plugin-router-express/src/index.js rename {plugin-router => plugin-router-express}/src/request.js (100%) create mode 100644 plugin-router-hapi/.babelrc create mode 100644 plugin-router-hapi/.eslintrc.js create mode 100644 plugin-router-hapi/package.json create mode 100644 plugin-router-hapi/src/index.js create mode 100644 plugin-router-hapi/src/request.js create mode 100644 plugin-router-http/.babelrc create mode 100644 plugin-router-http/.eslintrc.js create mode 100644 plugin-router-http/package.json create mode 100644 plugin-router-http/src/index.js create mode 100644 plugin-router-http/src/request.js create mode 100644 plugin-router-koa/.babelrc create mode 100644 plugin-router-koa/.eslintrc.js create mode 100644 plugin-router-koa/package.json create mode 100644 plugin-router-koa/src/index.js create mode 100644 plugin-router-koa/src/request.js diff --git a/plugin-router/.babelrc b/plugin-router-base/.babelrc similarity index 100% rename from plugin-router/.babelrc rename to plugin-router-base/.babelrc diff --git a/plugin-router/.eslintrc.js b/plugin-router-base/.eslintrc.js similarity index 100% rename from plugin-router/.eslintrc.js rename to plugin-router-base/.eslintrc.js diff --git a/plugin-router/package.json b/plugin-router-base/package.json similarity index 78% rename from plugin-router/package.json rename to plugin-router-base/package.json index 0bb3ae123..436576f37 100644 --- a/plugin-router/package.json +++ b/plugin-router-base/package.json @@ -1,11 +1,12 @@ { - "name": "@horizon/plugin-router", + "name": "@horizon/plugin-router-base", "version": "1.0.0", - "description": "Plugin router for Horizon.", + "description": "Base PluginRouter implementation for Horizon.", "main": "src/index.js", "scripts": { "lint": "eslint src test", - "test": "mocha test" + "test": "mocha dist/test", + "build": "babel src -d dist -s true" }, "repository": { "type": "git", @@ -21,7 +22,6 @@ "node": ">=4.0.0" }, "dependencies": { - "toposort-class": "^1.0.1" }, "peerDependencies": { "@horizon/server": "3.x" diff --git a/plugin-router/src/index.js b/plugin-router-base/src/index.js similarity index 92% rename from plugin-router/src/index.js rename to plugin-router-base/src/index.js index ea8c42c34..b8ed5bec4 100644 --- a/plugin-router/src/index.js +++ b/plugin-router-base/src/index.js @@ -5,16 +5,19 @@ const Toposort = require('toposort-class'); const EventEmitter = require('events'); class PluginRouter extends EventEmitter { - constructor(server) { + constructor(horizon) { super(); - this.server = server; - this.httpRoutes = {}; + this.horizon = horizon; this.methods = {}; this.plugins = new Map(); this.readyPlugins = new Set(); } + close() { + return Promise.all(Array.from(plugins.keys()).map((p) => this.remove(p))); + } + noteReady(plugin) { if (!this.readyPlugins.has(plugin)) { this.readyPlugins.add(plugin); @@ -35,22 +38,22 @@ class PluginRouter extends EventEmitter { } } - add(plugin) { + add(plugin, config) { if (this.plugins.has(plugin.name)) { return Promise.reject( new Error(`Plugin conflict: "${plugin.name}" already present.`)); } // Placeholder so we don't say we're ready too soon this.plugins.set(plugin.name, null); - this.plugins.set(plugin.name, Promise.resolve(this.server).then((server) => { + this.plugins.set(plugin.name, Promise.resolve().then(() => { this.emit('unready', this); if (plugin.activate.length > 1) { return plugin.activate( - server, + this.horizon, () => this.noteReady(plugin.name), () => this.noteUnready(plugin.name)); } else { - return Promise.resolve().then(() => plugin.activate(server)).then((x) => { + return Promise.resolve().then(() => plugin.activate(this.horizon)).then((x) => { this.noteReady(plugin.name); return x; }); @@ -75,6 +78,7 @@ class PluginRouter extends EventEmitter { return this.plugins.get(plugin.name); } + // `plugin` may be the name of the plugin, or the result of `add` remove(plugin, reason) { if (!this.plugins.has(plugin.name)) { return Promise.reject(new Error(`Plugin "${plugin.name}" is not present.`)); diff --git a/plugin-router-express/.babelrc b/plugin-router-express/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugin-router-express/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugin-router-express/.eslintrc.js b/plugin-router-express/.eslintrc.js new file mode 100644 index 000000000..535101a8a --- /dev/null +++ b/plugin-router-express/.eslintrc.js @@ -0,0 +1,16 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "max-len": [ ERROR, 130 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugin-router-express/package.json b/plugin-router-express/package.json new file mode 100644 index 000000000..9ed3cfc39 --- /dev/null +++ b/plugin-router-express/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugin-router-express", + "version": "1.0.0", + "description": "Plugin router for Horizon using Express as the backend.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/server": "3.x", + "express": "4.x" + }, + "devDependencies": { + "eslint": "^3.1.0", + "istanbul": "^0.4.3", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugin-router-express/src/index.js b/plugin-router-express/src/index.js new file mode 100644 index 000000000..668b67591 --- /dev/null +++ b/plugin-router-express/src/index.js @@ -0,0 +1,27 @@ +'use strict'; + +const PluginRouterBase = require('@horizon/plugin-router-base'); + +class PluginRouterExpress extends PluginRouterBase { + constructor(express, horizon) { + super(horizon); + this.express = express; + } + + add(...args) { + return super.add(...args).then((res) => { + // RSI: add routes to express + return res; + }); + } + + remove(...args) { + return super.remove(...args).then((res) => { + // RSI: remove routes from express + return res; + }); + } +}; + +module.exports = PluginRouterExpress; + diff --git a/plugin-router/src/request.js b/plugin-router-express/src/request.js similarity index 100% rename from plugin-router/src/request.js rename to plugin-router-express/src/request.js diff --git a/plugin-router-hapi/.babelrc b/plugin-router-hapi/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugin-router-hapi/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugin-router-hapi/.eslintrc.js b/plugin-router-hapi/.eslintrc.js new file mode 100644 index 000000000..535101a8a --- /dev/null +++ b/plugin-router-hapi/.eslintrc.js @@ -0,0 +1,16 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "max-len": [ ERROR, 130 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugin-router-hapi/package.json b/plugin-router-hapi/package.json new file mode 100644 index 000000000..3e150f227 --- /dev/null +++ b/plugin-router-hapi/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugin-router-hapi", + "version": "1.0.0", + "description": "Plugin router for Horizon using Hapi as the backend.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/server": "3.x", + "hapi": "15.x" + }, + "devDependencies": { + "eslint": "^3.1.0", + "istanbul": "^0.4.3", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugin-router-hapi/src/index.js b/plugin-router-hapi/src/index.js new file mode 100644 index 000000000..caf26be56 --- /dev/null +++ b/plugin-router-hapi/src/index.js @@ -0,0 +1,27 @@ +'use strict'; + +const PluginRouterBase = require('@horizon/plugin-router-base'); + +class PluginRouterHapi extends PluginRouterBase { + constructor(hapi, horizon) { + super(horizon); + this.hapi = hapi; + } + + add(...args) { + return super.add(...args).then((res) => { + // RSI: add routes to hapi + return res; + }); + } + + remove(...args) { + return super.remove(...args).then((res) => { + // RSI: remove routes from hapi + return res; + }); + } +}; + +module.exports = PluginRouterHapi; + diff --git a/plugin-router-hapi/src/request.js b/plugin-router-hapi/src/request.js new file mode 100644 index 000000000..f6c2ff1f7 --- /dev/null +++ b/plugin-router-hapi/src/request.js @@ -0,0 +1,25 @@ +'use strict'; + +const assert = require('assert'); + +class Request { + constructor(request, currentMethod) { + this.request_id = request.request_id; + this.options = request.options; + this.clientCtx = request.clientCtx; + this._parameters = request._parameters; + this._currentMethod = currentMethod; + Object.freeze(this); + } + + getParameter(methodName) { + return this._parameters[methodName]; + } + + setParameter(value) { + assert(this._currentMethod); + return this._parameters[this._currentMethod] = value; + } +} + +module.exports = Request; diff --git a/plugin-router-http/.babelrc b/plugin-router-http/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugin-router-http/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugin-router-http/.eslintrc.js b/plugin-router-http/.eslintrc.js new file mode 100644 index 000000000..535101a8a --- /dev/null +++ b/plugin-router-http/.eslintrc.js @@ -0,0 +1,16 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "max-len": [ ERROR, 130 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugin-router-http/package.json b/plugin-router-http/package.json new file mode 100644 index 000000000..99452813a --- /dev/null +++ b/plugin-router-http/package.json @@ -0,0 +1,36 @@ +{ + "name": "@horizon/plugin-router-http", + "version": "1.0.0", + "description": "Plugin router for Horizon using a basic HTTP backend.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/server": "3.x" + }, + "devDependencies": { + "eslint": "^3.1.0", + "istanbul": "^0.4.3", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugin-router-http/src/index.js b/plugin-router-http/src/index.js new file mode 100644 index 000000000..de053443d --- /dev/null +++ b/plugin-router-http/src/index.js @@ -0,0 +1,26 @@ +'use strict'; + +const PluginRouterBase = require('@horizon/plugin-router-base'); + +class PluginRouterHttp extends PluginRouterBase { + constructor(http, horizon) { + super(horizon); + this.http = http; + } + + add(...args) { + return super.add(...args).then((res) => { + // RSI: add routes to http server + return res; + }); + } + + remove(...args) { + return super.remove(...args).then((res) => { + // RSI: remove routes from http server + return res; + }); + } +}; + +module.exports = PluginRouterHttp; diff --git a/plugin-router-http/src/request.js b/plugin-router-http/src/request.js new file mode 100644 index 000000000..f6c2ff1f7 --- /dev/null +++ b/plugin-router-http/src/request.js @@ -0,0 +1,25 @@ +'use strict'; + +const assert = require('assert'); + +class Request { + constructor(request, currentMethod) { + this.request_id = request.request_id; + this.options = request.options; + this.clientCtx = request.clientCtx; + this._parameters = request._parameters; + this._currentMethod = currentMethod; + Object.freeze(this); + } + + getParameter(methodName) { + return this._parameters[methodName]; + } + + setParameter(value) { + assert(this._currentMethod); + return this._parameters[this._currentMethod] = value; + } +} + +module.exports = Request; diff --git a/plugin-router-koa/.babelrc b/plugin-router-koa/.babelrc new file mode 100644 index 000000000..c13c5f627 --- /dev/null +++ b/plugin-router-koa/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} diff --git a/plugin-router-koa/.eslintrc.js b/plugin-router-koa/.eslintrc.js new file mode 100644 index 000000000..535101a8a --- /dev/null +++ b/plugin-router-koa/.eslintrc.js @@ -0,0 +1,16 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "max-len": [ ERROR, 130 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugin-router-koa/package.json b/plugin-router-koa/package.json new file mode 100644 index 000000000..74263f753 --- /dev/null +++ b/plugin-router-koa/package.json @@ -0,0 +1,37 @@ +{ + "name": "@horizon/plugin-router-koa", + "version": "1.0.0", + "description": "Plugin router for Horizon using Koa as the backend.", + "main": "src/index.js", + "scripts": { + "lint": "eslint src test", + "test": "mocha dist/test", + "build": "babel src -d dist -s true" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rethinkdb/horizon.git" + }, + "author": "RethinkDB", + "license": "MIT", + "bugs": { + "url": "https://github.com/rethinkdb/horizon/issues" + }, + "homepage": "https://github.com/rethinkdb/horizon#readme", + "engines": { + "node": ">=4.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + "@horizon/server": "3.x", + "koa": "1.x" + }, + "devDependencies": { + "eslint": "^3.1.0", + "istanbul": "^0.4.3", + "mocha": "^2.5.3", + "babel-cli": "^6.11.4", + "babel-preset-es2015": "^6.9.0" + } +} diff --git a/plugin-router-koa/src/index.js b/plugin-router-koa/src/index.js new file mode 100644 index 000000000..12cec4156 --- /dev/null +++ b/plugin-router-koa/src/index.js @@ -0,0 +1,26 @@ +'use strict'; + +const PluginRouterBase = require('@horizon/plugin-router-base'); + +class PluginRouterKoa extends PluginRouterBase { + constructor(koa, horizon) { + super(horizon); + this.koa = koa; + } + + add(...args) { + return super.add(...args).then((res) => { + // RSI: add routes to koa server + return res; + }); + } + + remove(...args) { + return super.remove(...args).then((res) => { + // RSI: remove routes from koa server + return res; + }); + } +}; + +module.exports = PluginRouterKoa; diff --git a/plugin-router-koa/src/request.js b/plugin-router-koa/src/request.js new file mode 100644 index 000000000..f6c2ff1f7 --- /dev/null +++ b/plugin-router-koa/src/request.js @@ -0,0 +1,25 @@ +'use strict'; + +const assert = require('assert'); + +class Request { + constructor(request, currentMethod) { + this.request_id = request.request_id; + this.options = request.options; + this.clientCtx = request.clientCtx; + this._parameters = request._parameters; + this._currentMethod = currentMethod; + Object.freeze(this); + } + + getParameter(methodName) { + return this._parameters[methodName]; + } + + setParameter(value) { + assert(this._currentMethod); + return this._parameters[this._currentMethod] = value; + } +} + +module.exports = Request; diff --git a/server/src/client.js b/server/src/client.js index 0199726be..36642ace4 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -10,12 +10,14 @@ const websocket = require('ws'); class ClientConnection { constructor(socket, auth, - middlewareCb, + requestHandlerCb, + capabilitiesCb, clientEvents) { logger.debug('ClientConnection established.'); this.socket = socket; this.auth = auth; - this.middlewareCb = middlewareCb; + this.requestHandlerCb = requestHandlerCb; + this.capabilitiesCb = capabilitiesCb; this.clientEvents = clientEvents; this._context = { }; @@ -102,6 +104,7 @@ class ClientConnection { token: res.token, id: res.payload.id, provider: res.payload.provider, + capabilities: this.capabilitiesCb(), }); this.socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); @@ -138,11 +141,11 @@ class ClientConnection { this.responses.set(reqId, response); response.complete.then(() => this.remove_request(reqId)); - this.middlewareCb(raw_request, response, (err) => - response.end(err || new Error(`Request ran past the end of the middleware stack.`))); + this.requestHandlerCb(raw_request, response, (err) => + response.end(err || new Error('Request ran past the end of the ' + + 'request handler stack.'))); } - remove_response(request_id) { const response = this.responses.get(request_id); this.responses.delete(request_id); diff --git a/server/src/schema/server_options.js b/server/src/schema/server_options.js index 5ba26e098..3246d87a5 100644 --- a/server/src/schema/server_options.js +++ b/server/src/schema/server_options.js @@ -30,4 +30,10 @@ const auth = Joi.object({ allow_unauthenticated: Joi.boolean().default(false), }).unknown(false); -module.exports = {server, auth}; +const method = Joi.object({ + type: Joi.valid('middleware', 'option', 'prereq', 'terminal').required(), + handler: Joi.func().minArity(2).maxArity(3).required(), + requires: Joi.array().single().items(Joi.string()).default([]), +}).unknown(false); + +module.exports = {server, auth, method}; diff --git a/server/src/server.js b/server/src/server.js index b8e2e7a60..85d3cfa55 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -4,12 +4,15 @@ const Auth = require('./auth').Auth; const ClientConnection = require('./client'); const logger = require('./logger'); const {ReliableConn, ReliableChangefeed} = require('./reliable'); -const optionsSchema = require('./schema/server_options').server; +const {optionsSchema, methodSchema} = require('./schema/server_options'); +const Request = require('./request'); const EventEmitter = require('events'); + const Joi = require('joi'); const websocket = require('ws'); const r = require('rethinkdb'); +const Toposort = require('toposort-class'); const protocolName = 'rethinkdb-horizon-v1'; @@ -89,7 +92,8 @@ class Server extends EventEmitter { const client = new ClientConnection( socket, this._auth, - this._middlewareCb, + this._getRequestHandler, + this._getCapabilities, this // Used to emit a client auth event ); this._clients.add(client); @@ -116,10 +120,6 @@ class Server extends EventEmitter { } } - makeReliableChangefeed(reql, ...args) { - return new ReliableChangefeed(reql, this._reliableConn, ...args); - } - auth() { return this._auth; } @@ -128,12 +128,127 @@ class Server extends EventEmitter { return this._reliableConn; } - set_middleware(mw) { - this._middlewareCb = mw ? mw : this._defaultMiddlewareCb; + add_method(name, raw_options) { + const options = Joi.attempt(raw_options, optionsSchema); + if (this._methods[name]) { + throw new Error(`"${name}" is already registered as a method.`); + } + this._methods[name] = raw_options; + + if (options.type === 'middleware') { + this._middlewareMethods.add(name); + } + + this._requirementsOrdering = null; + this._capabilities = null; + } + + remove_method(name) { + delete this._methods[name]; + this._middlewareMethods.delete(name); + this._requirementsOrdering = null; + this._capabilities = null; + } + + _getRequirementsOrdering() { + if (!this._requirementsOrdering) { + this._requirementsOrdering = {}; + + const topo = new Toposort(); + for (const m in this._methods) { + const reqs = this._methods[m].requires; + topo.add(m, reqs); + for (const r of reqs) { + if (!this._methods[r]) { + throw new Error( + `Missing method "${r}", which is required by method "${m}".`); + } + } + } + + this._requirementsOrdering = topo.sort().reverse(); + } + return this._requirementsOrdering; + } + + _getCapabilities() { + if (!this._capabilities) { + this._capabilities = {options: [], terminals: []}; + for (const k in this._methods) { + const method = this._methods[k]; + switch (method.type) { + case 'option': + this._capabilities.options.push(k); + break; + case 'terminal': + this._capabilities.terminals.push(k); + break; + default: + break; + } + } + } + return this._capabilities; + } + + _getRequestHandler() { + return (req, res, next) => { + let terminal = null; + const requirements = {}; + + this._middlewareMethods.forEach((name) => { + requirements[name] = true; + }); + + for (const o in req.options) { + const m = this._methods[o]; + if (!m) { + next(new Error(`No method to handle option "${o}".`)); + return; + } + + if (m.type === 'terminal') { + if (terminal !== null) { + next(new Error('Multiple terminal methods in request: ' + + `"${terminal}", "${o}".`)); + return; + } + terminal = o; + } else { + requirements[o] = true; + } + for (const r of m.requires) { + requirements[r] = true; + } + } + + if (terminal === null) { + next(new Error('No terminal method was specified in the request.')); + } else if (requirements[terminal]) { + next(new Error(`Terminal method "${terminal}" is also a requirement.`)); + } else { + const ordering = this.requirementsOrdering(); + const chain = Object.keys(requirements).sort( + (a, b) => ordering[a] - ordering[b]); + chain.push(terminal); + + chain.reduceRight((cb, methodName) => + (maybeErr) => { + if (maybeErr instanceof Error) { + next(maybeErr); + } else { + try { + this._methods[methodName].handler(new Request(req, methodName), res, cb); + } catch (e) { + next(e); + } + } + }, next)(); + } + }; } - // TODO: We close clients in `onUnready` above, but don't wait for - // them to be closed. + // TODO: We close clients in `onUnready` above, but don't wait for them to be closed. close() { if (!this._close_promise) { this._close_promise = diff --git a/test/package.json b/test/package.json index 65b31b682..c90b0de69 100644 --- a/test/package.json +++ b/test/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "horizon": "3.0.0-alpha-0", - "@horizon/plugin-router": "1.0.0", + "@horizon/plugin-router-base": "1.0.0", "@horizon-plugins/defaults": "1.0.0", "@horizon/server": "3.0.0-alpha-0", "argparse": "^1.0.7" diff --git a/test/src/serve.js b/test/src/serve.js index e4d7da38d..815023b30 100755 --- a/test/src/serve.js +++ b/test/src/serve.js @@ -177,14 +177,13 @@ new Promise((resolve) => { token_secret: crypto.randomBytes(64).toString('base64'), }, }); - console.log('starting http servers'); const pluginRouter = new PluginRouter(hz_server); - pluginRouter.add(plugins({ + pluginRouter.add(plugins, { permissions: 'permit-all', auto_create_collection: true, auto_create_index: true, - })).catch((err) => + }).catch((err) => console.log(`Plugin initialization failed: ${err.stack}`) ); @@ -192,6 +191,7 @@ new Promise((resolve) => { hz_server.set_middleware(pluginRouter.hzMiddleware()); // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server + console.log('starting http servers'); http_servers.forEach((serv, i) => { const extant_listeners = serv.listeners('request').slice(0); serv.removeAllListeners('request'); diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 7370a4a55..9468048be 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -128,6 +128,7 @@ const populate_collection = (collection, rows) => { const start_horizon_server = (done) => { logger.info('creating http server'); assert.strictEqual(horizon_server, undefined); + assert.strictEqual(plugin_router, undefined); const http_server = new http.Server(); http_server.listen(0, () => { @@ -142,7 +143,7 @@ const start_horizon_server = (done) => { }, }); - const plugin_router = new PluginRouter(horizon_server); + plugin_router = new PluginRouter(horizon_server); const plugins_promise = plugin_router.add(defaults({ auto_create_collection: true, auto_create_index: true, @@ -162,6 +163,11 @@ const start_horizon_server = (done) => { }; const close_horizon_server = () => { + if (plugin_router !== undefined) { + plugin_router.close(); + } + plugin_router = undefined; + if (horizon_server !== undefined) { horizon_server.removeAllListeners('ready'); horizon_server.removeAllListeners('unready'); From 02cfdb974d8fb4d5eb5d99252cb31ccf95705412 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 8 Sep 2016 18:13:59 -0700 Subject: [PATCH 75/86] no longer pass server to plugins, but a curated plugin context --- plugin-router-base/src/index.js | 191 ------------------ .../base}/.babelrc | 0 .../base}/.eslintrc.js | 0 .../base}/package.json | 0 plugin-router/base/src/index.js | 107 ++++++++++ .../express}/.babelrc | 0 .../express}/.eslintrc.js | 0 .../express}/package.json | 0 .../express}/src/index.js | 0 .../express}/src/request.js | 0 .../hapi}/.babelrc | 0 .../hapi}/.eslintrc.js | 0 .../hapi}/package.json | 0 .../hapi}/src/index.js | 0 .../hapi}/src/request.js | 0 .../http}/.babelrc | 0 .../http}/.eslintrc.js | 0 .../http}/package.json | 0 .../http}/src/index.js | 0 .../http}/src/request.js | 0 .../koa}/.babelrc | 0 .../koa}/.eslintrc.js | 0 .../koa}/package.json | 0 .../koa}/src/index.js | 0 .../koa}/src/request.js | 0 plugins/above/package.json | 2 +- plugins/above/src/above.js | 4 +- plugins/below/package.json | 2 +- plugins/below/src/below.js | 4 +- plugins/collection/package.json | 2 +- plugins/collection/src/collection.js | 54 +++-- plugins/collection/src/types/metadata.js | 6 +- plugins/defaults/package.json | 2 +- plugins/defaults/src/defaults.js | 111 +++++----- plugins/fetch/package.json | 2 +- plugins/fetch/src/fetch.js | 12 +- plugins/find/package.json | 2 +- plugins/find/src/find.js | 4 +- plugins/findAll/package.json | 2 +- plugins/findAll/src/findAll.js | 4 +- plugins/insert/package.json | 2 +- plugins/insert/src/insert.js | 12 +- plugins/limit/package.json | 2 +- plugins/limit/src/limit.js | 4 +- plugins/order/package.json | 2 +- plugins/order/src/order.js | 4 +- plugins/permissions/package.json | 5 +- plugins/permissions/src/permissions.js | 152 +++++++------- plugins/permissions/src/template.js | 1 + plugins/permit-all/package.json | 2 +- plugins/permit-all/src/permit-all.js | 4 +- plugins/remove/package.json | 2 +- plugins/remove/src/remove.js | 12 +- plugins/replace/package.json | 2 +- plugins/replace/src/replace.js | 12 +- plugins/store/package.json | 2 +- plugins/store/src/store.js | 12 +- plugins/timeout/package.json | 2 +- plugins/timeout/src/timeout.js | 4 +- plugins/update/package.json | 2 +- plugins/update/src/update.js | 12 +- plugins/upsert/package.json | 2 +- plugins/upsert/src/upsert.js | 12 +- plugins/watch/package.json | 2 +- plugins/watch/src/watch.js | 12 +- server/package.json | 1 + server/src/auth.js | 2 +- server/src/request.js | 25 +++ server/src/schema/server_options.js | 4 +- server/src/server.js | 54 ++--- setupDev.sh | 2 +- test/package.json | 2 +- test/src/serve.js | 11 +- test/src/test/utils.js | 22 +- 74 files changed, 426 insertions(+), 488 deletions(-) delete mode 100644 plugin-router-base/src/index.js rename {plugin-router-base => plugin-router/base}/.babelrc (100%) rename {plugin-router-base => plugin-router/base}/.eslintrc.js (100%) rename {plugin-router-base => plugin-router/base}/package.json (100%) create mode 100644 plugin-router/base/src/index.js rename {plugin-router-express => plugin-router/express}/.babelrc (100%) rename {plugin-router-express => plugin-router/express}/.eslintrc.js (100%) rename {plugin-router-express => plugin-router/express}/package.json (100%) rename {plugin-router-express => plugin-router/express}/src/index.js (100%) rename {plugin-router-express => plugin-router/express}/src/request.js (100%) rename {plugin-router-hapi => plugin-router/hapi}/.babelrc (100%) rename {plugin-router-hapi => plugin-router/hapi}/.eslintrc.js (100%) rename {plugin-router-hapi => plugin-router/hapi}/package.json (100%) rename {plugin-router-hapi => plugin-router/hapi}/src/index.js (100%) rename {plugin-router-hapi => plugin-router/hapi}/src/request.js (100%) rename {plugin-router-http => plugin-router/http}/.babelrc (100%) rename {plugin-router-http => plugin-router/http}/.eslintrc.js (100%) rename {plugin-router-http => plugin-router/http}/package.json (100%) rename {plugin-router-http => plugin-router/http}/src/index.js (100%) rename {plugin-router-http => plugin-router/http}/src/request.js (100%) rename {plugin-router-koa => plugin-router/koa}/.babelrc (100%) rename {plugin-router-koa => plugin-router/koa}/.eslintrc.js (100%) rename {plugin-router-koa => plugin-router/koa}/package.json (100%) rename {plugin-router-koa => plugin-router/koa}/src/index.js (100%) rename {plugin-router-koa => plugin-router/koa}/src/request.js (100%) create mode 100644 server/src/request.js diff --git a/plugin-router-base/src/index.js b/plugin-router-base/src/index.js deleted file mode 100644 index b8ed5bec4..000000000 --- a/plugin-router-base/src/index.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; - -const Request = require('./request'); -const Toposort = require('toposort-class'); -const EventEmitter = require('events'); - -class PluginRouter extends EventEmitter { - constructor(horizon) { - super(); - this.horizon = horizon; - this.methods = {}; - - this.plugins = new Map(); - this.readyPlugins = new Set(); - } - - close() { - return Promise.all(Array.from(plugins.keys()).map((p) => this.remove(p))); - } - - noteReady(plugin) { - if (!this.readyPlugins.has(plugin)) { - this.readyPlugins.add(plugin); - this.emit('pluginReady', plugin, this); - if (this.readyPlugins.size === this.plugins.size) { - setImmediate(() => this.emit('ready', this)); - } - } - } - - noteUnready(plugin) { - if (this.readyPlugins.has(plugin)) { - this.readyPlugins.delete(plugin); - this.emit('pluginUnready', plugin, this); - if (this.readyPlugins.size === this.plugins.size - 1) { - this.emit('unready', this); - } - } - } - - add(plugin, config) { - if (this.plugins.has(plugin.name)) { - return Promise.reject( - new Error(`Plugin conflict: "${plugin.name}" already present.`)); - } - // Placeholder so we don't say we're ready too soon - this.plugins.set(plugin.name, null); - this.plugins.set(plugin.name, Promise.resolve().then(() => { - this.emit('unready', this); - if (plugin.activate.length > 1) { - return plugin.activate( - this.horizon, - () => this.noteReady(plugin.name), - () => this.noteUnready(plugin.name)); - } else { - return Promise.resolve().then(() => plugin.activate(this.horizon)).then((x) => { - this.noteReady(plugin.name); - return x; - }); - } - }).then((active) => { - if (this.httpRoutes[plugin.name]) { - throw new Error(`Plugin conflict: "${plugin.name}" already present.`); - } - - for (const m in active.methods) { - if (this.methods[m]) { - throw new Error(`Method name conflict: "${m}"`); - } - } - - this.httpRoutes[plugin.name] = active; - for (const m in active.methods) { - this.methods[m] = active.methods[m]; - this._requirementsOrdering = null; - } - })); - return this.plugins.get(plugin.name); - } - - // `plugin` may be the name of the plugin, or the result of `add` - remove(plugin, reason) { - if (!this.plugins.has(plugin.name)) { - return Promise.reject(new Error(`Plugin "${plugin.name}" is not present.`)); - } - return this.plugins.get(plugin.name).then((active) => { - for (const m in active.methods) { - delete this.methods[m]; - this._requirementsOrdering = null; - } - if (plugin.deactivate) { - plugin.deactivate(reason || 'Removed from PluginRouter.'); - } - }); - } - - requirementsOrdering() { - // RSI: move dependencies and topological sorting into the server - if (!this._requirementsOrdering) { - this._requirementsOrdering = {}; - - const topo = new Toposort(); - for (const m in this.methods) { - const reqs = this.methods[m].requires; - if (reqs) { - topo.add(m, reqs); - for (const r of reqs) { - if (!this.methods[r]) { - throw new Error( - `Missing method "${r}", which is required by method "${m}".`); - } - } - } - } - - this._requirementsOrdering = topo.sort().reverse(); - } - return this._requirementsOrdering; - } - - httpMiddleware() { - return (req, res, next) => { - const pathParts = req.path.split('/'); - const name = pathParts[0] || pathParts[1]; - const plugin = this.httpRoutes[name]; - if (plugin && plugin.httpRoute) { - plugin.httpRouter(req, res, next); - } else { - next(); - } - }; - } - - hzMiddleware() { - return (req, res, next) => { - let terminalName = null; - const requirements = {}; - if (req.options) { - for (const o in req.options) { - const m = this.methods[o]; - if (m) { - if (m.type === 'terminal') { - if (terminalName !== null) { - next(new Error('Multiple terminal methods in request: ' + - `"${terminalName}", "${o}"`)); - } else { - terminalName = o; - } - } else { - requirements[o] = true; - } - if (m.requires) { - for (const r of m.requires) { - requirements[r] = true; - } - } - } else { - next(new Error(`No method to handle option "${o}".`)); - } - } - } - - if (terminalName === null) { - next(new Error('No terminal method was specified in the request.')); - } else if (requirements[terminalName]) { - next(new Error(`Terminal method "${terminalName}" is also a requirement.`)); - } else { - const ordering = this.requirementsOrdering(); - const middlewareChain = Object.keys(requirements).sort( - (a, b) => ordering[a] - ordering[b]); - middlewareChain.push(terminalName); - - middlewareChain.reduceRight((cb, methodName) => - (maybeErr) => { - if (maybeErr instanceof Error) { - next(maybeErr); - } else { - try { - this.methods[methodName].handler(new Request(req, methodName), res, cb); - } catch (e) { - next(e); - } - } - }, next)(); - } - }; - } -} - -module.exports = PluginRouter; - diff --git a/plugin-router-base/.babelrc b/plugin-router/base/.babelrc similarity index 100% rename from plugin-router-base/.babelrc rename to plugin-router/base/.babelrc diff --git a/plugin-router-base/.eslintrc.js b/plugin-router/base/.eslintrc.js similarity index 100% rename from plugin-router-base/.eslintrc.js rename to plugin-router/base/.eslintrc.js diff --git a/plugin-router-base/package.json b/plugin-router/base/package.json similarity index 100% rename from plugin-router-base/package.json rename to plugin-router/base/package.json diff --git a/plugin-router/base/src/index.js b/plugin-router/base/src/index.js new file mode 100644 index 000000000..3d0419f21 --- /dev/null +++ b/plugin-router/base/src/index.js @@ -0,0 +1,107 @@ +'use strict'; + +const EventEmitter = require('events'); + +class PluginRouter extends EventEmitter { + constructor(horizon) { + super(); + this.horizon = horizon; + this.plugins = new Map(); + this.readyPlugins = new Set(); + this.context = { + horizon: { + options: horizon.options, + auth: horizon.auth(), + rdbConnection: horizon.rdbConnection, + events: horizon.events, + }, + }; + } + + close() { + return Promise.all(Array.from(this.plugins.keys()).map((p) => this.remove(p))); + } + + add(plugin, options) { + if (!options.name) { + options.name = plugin.name; + } + + if (this.plugins.has(options.name)) { + return Promise.reject( + new Error(`Plugin conflict: "${options.name}" already present.`)); + } + + // Placeholder so we don't say we're ready too soon + this.plugins.set(options.name, null); + this.emit('unready', this); + + const activatePromise = Promise.resolve().then(() => + plugin.activate(this.context, options, + () => this._noteReady(options.name), + () => this._noteUnready(options.name)) + ).then((active) => { + const addedMethods = []; + try { + for (const m in active.methods) { + this.horizon.addMethod(m, active.methods[m]); + addedMethods.push(m); + } + } catch (err) { + // Back out and clean up safely if any methods failed to add + addedMethods.forEach((m) => this.horizon.removeMethod(m)); + throw err; + } + return active; + }); + + if (plugin.activate.length < 3) { + activatePromise.then(() => this._noteReady(options.name)); + } + + this.plugins.set(options.name, {options, activatePromise}); + return activatePromise; + } + + remove(name, reason) { + const plugin = this.plugins.get(name); + + if (!plugin) { + return Promise.reject(new Error(`Plugin "${name}" is not present.`)); + } + + this.plugins.delete(name); + return plugin.activatePromise.then((active) => { + for (const m in active.methods) { + this.horizon.removeMethod(m); + } + if (plugin.deactivate) { + return plugin.deactivate(this.context, plugin.options, + reason || 'Removed from PluginRouter.'); + } + }); + } + + _noteReady(plugin) { + if (!this.readyPlugins.has(plugin)) { + this.readyPlugins.add(plugin); + this.emit('pluginReady', plugin, this); + if (this.readyPlugins.size === this.plugins.size) { + setImmediate(() => this.emit('ready', this)); + } + } + } + + _noteUnready(plugin) { + if (this.readyPlugins.has(plugin)) { + this.readyPlugins.delete(plugin); + this.emit('pluginUnready', plugin, this); + if (this.readyPlugins.size === this.plugins.size - 1) { + this.emit('unready', this); + } + } + } +} + +module.exports = PluginRouter; + diff --git a/plugin-router-express/.babelrc b/plugin-router/express/.babelrc similarity index 100% rename from plugin-router-express/.babelrc rename to plugin-router/express/.babelrc diff --git a/plugin-router-express/.eslintrc.js b/plugin-router/express/.eslintrc.js similarity index 100% rename from plugin-router-express/.eslintrc.js rename to plugin-router/express/.eslintrc.js diff --git a/plugin-router-express/package.json b/plugin-router/express/package.json similarity index 100% rename from plugin-router-express/package.json rename to plugin-router/express/package.json diff --git a/plugin-router-express/src/index.js b/plugin-router/express/src/index.js similarity index 100% rename from plugin-router-express/src/index.js rename to plugin-router/express/src/index.js diff --git a/plugin-router-express/src/request.js b/plugin-router/express/src/request.js similarity index 100% rename from plugin-router-express/src/request.js rename to plugin-router/express/src/request.js diff --git a/plugin-router-hapi/.babelrc b/plugin-router/hapi/.babelrc similarity index 100% rename from plugin-router-hapi/.babelrc rename to plugin-router/hapi/.babelrc diff --git a/plugin-router-hapi/.eslintrc.js b/plugin-router/hapi/.eslintrc.js similarity index 100% rename from plugin-router-hapi/.eslintrc.js rename to plugin-router/hapi/.eslintrc.js diff --git a/plugin-router-hapi/package.json b/plugin-router/hapi/package.json similarity index 100% rename from plugin-router-hapi/package.json rename to plugin-router/hapi/package.json diff --git a/plugin-router-hapi/src/index.js b/plugin-router/hapi/src/index.js similarity index 100% rename from plugin-router-hapi/src/index.js rename to plugin-router/hapi/src/index.js diff --git a/plugin-router-hapi/src/request.js b/plugin-router/hapi/src/request.js similarity index 100% rename from plugin-router-hapi/src/request.js rename to plugin-router/hapi/src/request.js diff --git a/plugin-router-http/.babelrc b/plugin-router/http/.babelrc similarity index 100% rename from plugin-router-http/.babelrc rename to plugin-router/http/.babelrc diff --git a/plugin-router-http/.eslintrc.js b/plugin-router/http/.eslintrc.js similarity index 100% rename from plugin-router-http/.eslintrc.js rename to plugin-router/http/.eslintrc.js diff --git a/plugin-router-http/package.json b/plugin-router/http/package.json similarity index 100% rename from plugin-router-http/package.json rename to plugin-router/http/package.json diff --git a/plugin-router-http/src/index.js b/plugin-router/http/src/index.js similarity index 100% rename from plugin-router-http/src/index.js rename to plugin-router/http/src/index.js diff --git a/plugin-router-http/src/request.js b/plugin-router/http/src/request.js similarity index 100% rename from plugin-router-http/src/request.js rename to plugin-router/http/src/request.js diff --git a/plugin-router-koa/.babelrc b/plugin-router/koa/.babelrc similarity index 100% rename from plugin-router-koa/.babelrc rename to plugin-router/koa/.babelrc diff --git a/plugin-router-koa/.eslintrc.js b/plugin-router/koa/.eslintrc.js similarity index 100% rename from plugin-router-koa/.eslintrc.js rename to plugin-router/koa/.eslintrc.js diff --git a/plugin-router-koa/package.json b/plugin-router/koa/package.json similarity index 100% rename from plugin-router-koa/package.json rename to plugin-router/koa/package.json diff --git a/plugin-router-koa/src/index.js b/plugin-router/koa/src/index.js similarity index 100% rename from plugin-router-koa/src/index.js rename to plugin-router/koa/src/index.js diff --git a/plugin-router-koa/src/request.js b/plugin-router/koa/src/request.js similarity index 100% rename from plugin-router-koa/src/request.js rename to plugin-router/koa/src/request.js diff --git a/plugins/above/package.json b/plugins/above/package.json index ffcf1ed3b..b6822eb2f 100644 --- a/plugins/above/package.json +++ b/plugins/above/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/above/src/above.js b/plugins/above/src/above.js index 494a6cba3..db0d90611 100644 --- a/plugins/above/src/above.js +++ b/plugins/above/src/above.js @@ -16,7 +16,7 @@ function above(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_above', activate: () => ({ methods: { @@ -26,4 +26,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/below/package.json b/plugins/below/package.json index 5302d247b..3aa079de1 100644 --- a/plugins/below/package.json +++ b/plugins/below/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/below/src/below.js b/plugins/below/src/below.js index 79b7f9630..0eaee83b6 100644 --- a/plugins/below/src/below.js +++ b/plugins/below/src/below.js @@ -16,7 +16,7 @@ function below(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_below', activate: () => ({ methods: { @@ -26,4 +26,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/collection/package.json b/plugins/collection/package.json index ec044fb6f..1ca4aa811 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 2e66b42de..09986bfcb 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -40,37 +40,33 @@ function collection(metadata) { }; } -module.exports = (options) => { - const metadata = Symbol('hz_collection_metadata'); +module.exports = { + name: 'hz_collection', + activate: (context, options, onReady, onUnready) => { + context[options.name] = new ReliableMetadata( + context, + Boolean(options.auto_create_collection), + Boolean(options.auto_create_index)); - return { - name: 'hz_collection', - activate: (ctx, onReady, onUnready) => { - ctx[metadata] = new ReliableMetadata( - ctx, - Boolean(options.auto_create_collection), - Boolean(options.auto_create_index)); - - return new Promise((resolve, reject) => { - ctx[metadata].subscribe({onUnready, onReady: () => { - resolve({ - methods: { - collection: { - type: 'option', - handler: collection(ctx[metadata]), - }, + return new Promise((resolve, reject) => { + context[options.name].subscribe({onUnready, onReady: () => { + resolve({ + methods: { + collection: { + type: 'option', + handler: collection(context[options.name]), }, - }); - onReady(); - }}); - }); - }, - deactivate: (ctx) => { - if (ctx[metadata]) { - ctx[metadata].close(); - } - }, - }; + }, + }); + onReady(); + }}); + }); + }, + deactivate: (context, options) => { + if (context[options.name]) { + context[options.name].close(); + } + }, }; module.exports.createCollection = queries.createCollection; diff --git a/plugins/collection/src/types/metadata.js b/plugins/collection/src/types/metadata.js index a960d5bca..59b8bde1f 100644 --- a/plugins/collection/src/types/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -130,12 +130,12 @@ class ReliableInit extends Reliable { } class ReliableMetadata extends Reliable { - constructor(server, + constructor(context, auto_create_collection, auto_create_index) { super(); - this._db = server.options.project_name; - this._reliable_conn = server.rdb_connection(); + this._db = context.horizon.options.project_name; + this._reliable_conn = context.horizon.rdbConnection; this._auto_create_collection = auto_create_collection; this._auto_create_index = auto_create_index; this._collections = new Map(); diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json index a177e760b..300f90136 100644 --- a/plugins/defaults/package.json +++ b/plugins/defaults/package.json @@ -42,7 +42,7 @@ "@horizon-plugins/watch": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/defaults/src/defaults.js b/plugins/defaults/src/defaults.js index b98a734cb..82ce0c399 100644 --- a/plugins/defaults/src/defaults.js +++ b/plugins/defaults/src/defaults.js @@ -26,72 +26,77 @@ const defaultPermissions = { 'permit-all': require('@horizon-plugins/permit-all'), }; -// Combines some subset of the default plugins into a single plugin for ease-of-use -// `raw_config` can be omitted or an object with any or all of these properties: +// Combines some subset of the default plugins into a single plugin for ease-of-use. +// `options` may have any or all of these properties: // `methods`: an array of default methods to include, defaults to all of them // `permissions`: // false: no permissions plugin will be loaded (the collections API won't work // unless some other plugin provides the 'hz_permissions' prereq) // 'permissions': the standard permissions plugin will be loaded (default) // 'permit-all': a dummy permissions plugin will be loaded that allows all requests -module.exports = (raw_config) => { - const config = raw_config || {}; - const subplugins = (config.methods || Object.keys(defaultMethods)).map((name) => { - const plugin = defaultMethods[name]; - if (!plugin) { - throw new Error(`Method "${name}" is not provided by a default Horizon plugin.`); - } - return plugin(config); - }); +module.exports = { + name: 'hz_defaults', + activate: (context, options, onReady, onUnready) => { + const subplugins = (options.methods || Object.keys(defaultMethods)).map((name) => { + const plugin = defaultMethods[name]; + if (!plugin) { + throw new Error(`Method "${name}" is not provided by a default Horizon plugin.`); + } + return plugin; + }); + context[options.name] = {subplugins}; - if (config.permissions === undefined) { - // Use the secure thing by default - subplugins.push(defaultPermissions.permissions(config)); - } else if (config.permissions !== false) { - const plugin = defaultPermissions[config.permissions]; - if (!plugin) { - throw new Error(`Unrecognized permissions plugin name "${config.permissions}", ` + - 'expected "permissions" or "permit-all".'); + if (options.permissions === undefined) { + // Use the secure thing by default + subplugins.push(defaultPermissions.permissions); + } else if (options.permissions !== false) { + const plugin = defaultPermissions[options.permissions]; + if (!plugin) { + throw new Error(`Unrecognized permissions plugin name "${options.permissions}", ` + + 'expected "permissions" or "permit-all".'); + } + subplugins.push(plugin); } - subplugins.push(plugin(config)); - } - return { - name: 'hz_defaults', - activate: (ctx, onReady, onUnready) => { - // Some subplugins may need to notify about readiness - const readyPlugins = new Map(); - function ready(name) { - readyPlugins.set(name); - if (readyPlugins.size === subplugins.length) { - onReady(); - } + // Some subplugins may need to notify about readiness + const readyPlugins = new Map(); + function ready(name) { + readyPlugins.set(name); + if (readyPlugins.size === subplugins.length) { + onReady(); } - function unready(name) { - if (readyPlugins.size === subplugins.length) { - onUnready(); - } - readyPlugins.delete(name); + } + function unready(name) { + if (readyPlugins.size === subplugins.length) { + onUnready(); + } + readyPlugins.delete(name); + } + + const promises = subplugins.map((plugin) => { + const promise = Promise.resolve().then(() => + plugin.activate(context, + options, + () => ready(plugin.name), + () => unready(plugin.name)) + ); + if (plugin.activate.length < 3) { + promise.then(() => ready(plugin.name)); } + return promise; + }); - const promises = subplugins.map((plugin) => { - const promise = Promise.resolve().then(() => - plugin.activate(ctx, () => ready(plugin.name), () => unready(plugin.name)) - ); - if (plugin.activate.length < 2) { - ready(plugin.name); - } - return promise; - }); + return Promise.all(promises).then((results) => ({ + methods: Object.assign({}, ...results.map((i) => i.methods)), + })); + }, - return Promise.all(promises).then((results) => ({ - methods: Object.assign({}, ...results.map((i) => i.methods)), - })); - }, - deactivate: (ctx) => - Promise.all(subplugins.map((p) => - Promise.resolve().then(() => p.deactivate && p.deactivate(ctx)))), - }; + deactivate: (context, options) => { + const subplugins = context[options.name].subplugins; + delete context[options.name]; + return Promise.all(subplugins.map((p) => + Promise.resolve().then(() => p.deactivate && p.deactivate(context, options)))); + }, }; module.exports.methods = defaultMethods; diff --git a/plugins/fetch/package.json b/plugins/fetch/package.json index 9547dee69..d6324c544 100644 --- a/plugins/fetch/package.json +++ b/plugins/fetch/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/fetch/src/fetch.js b/plugins/fetch/src/fetch.js index 4f8d3d317..e51bfa4b9 100644 --- a/plugins/fetch/src/fetch.js +++ b/plugins/fetch/src/fetch.js @@ -2,11 +2,11 @@ const {reqlOptions, reads} = require('@horizon/plugin-utils'); -function fetch(ctx) { +function fetch(context) { return (req, res, next) => { const args = req.options.fetch; const permissions = req.getParameter('hz_permissions'); - const conn = ctx.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); if (args.length !== 0) { next(new Error(`"fetch" expects 0 arguments but found ${args.length}`)); @@ -53,15 +53,15 @@ function fetch(ctx) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_fetch', - activate: (ctx) => ({ + activate: (context) => ({ methods: { fetch: { type: 'terminal', requires: ['hz_permissions'], - handler: fetch(ctx), + handler: fetch(context), }, }, }), -}); +}; diff --git a/plugins/find/package.json b/plugins/find/package.json index ab83cab3c..22a297474 100644 --- a/plugins/find/package.json +++ b/plugins/find/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/find/src/find.js b/plugins/find/src/find.js index c1e0f5a0a..881cee7c5 100644 --- a/plugins/find/src/find.js +++ b/plugins/find/src/find.js @@ -14,7 +14,7 @@ function find(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_find', activate: () => ({ methods: { @@ -24,4 +24,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/findAll/package.json b/plugins/findAll/package.json index 9819dab68..37283be0c 100644 --- a/plugins/findAll/package.json +++ b/plugins/findAll/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/findAll/src/findAll.js b/plugins/findAll/src/findAll.js index 2aaac0bbe..cd19e30ac 100644 --- a/plugins/findAll/src/findAll.js +++ b/plugins/findAll/src/findAll.js @@ -14,7 +14,7 @@ function findAll(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_findAll', activate: () => ({ methods: { @@ -24,4 +24,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/insert/package.json b/plugins/insert/package.json index be8ae6629..161c05280 100644 --- a/plugins/insert/package.json +++ b/plugins/insert/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/insert/src/insert.js b/plugins/insert/src/insert.js index 3474f6a3f..2da6befbc 100644 --- a/plugins/insert/src/insert.js +++ b/plugins/insert/src/insert.js @@ -3,9 +3,9 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); -function insert(server) { +function insert(context) { return (request, response, next) => { - const conn = server.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -33,15 +33,15 @@ function insert(server) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_insert', - activate: (ctx) => ({ + activate: (context) => ({ methods: { insert: { type: 'terminal', requires: ['hz_permissions'], - handler: insert(ctx), + handler: insert(context), }, }, }), -}); +}; diff --git a/plugins/limit/package.json b/plugins/limit/package.json index 294ed3d9e..d78da6a91 100644 --- a/plugins/limit/package.json +++ b/plugins/limit/package.json @@ -24,7 +24,7 @@ "dependencies": { }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/limit/src/limit.js b/plugins/limit/src/limit.js index f13dc0856..1c412d6c9 100644 --- a/plugins/limit/src/limit.js +++ b/plugins/limit/src/limit.js @@ -12,7 +12,7 @@ function limit(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_limit', activate: () => ({ methods: { @@ -22,4 +22,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/order/package.json b/plugins/order/package.json index c8ca3cb04..fa4e5a5cb 100644 --- a/plugins/order/package.json +++ b/plugins/order/package.json @@ -24,7 +24,7 @@ "dependencies": { }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/order/src/order.js b/plugins/order/src/order.js index e42cfd98d..3c1e60b93 100644 --- a/plugins/order/src/order.js +++ b/plugins/order/src/order.js @@ -30,7 +30,7 @@ function order(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_order', activate: () => ({ methods: { @@ -40,4 +40,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index e83bdcd9a..3618e0a0a 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -23,10 +23,11 @@ }, "dependencies": { "@horizon/plugin-utils": "1.0.0", - "@horizon/client": "3.0.0-alpha-0" + "@horizon/client": "3.0.0-alpha-0", + "joi": "^8.0.4" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 6d7ffbbe7..02fedb7a6 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -4,6 +4,8 @@ const Rule = require('./rule'); const assert = require('assert'); +const Joi = require('joi'); + const {r, logger, ReliableChangefeed} = require('@horizon/server'); // We can be desynced from the database for up to 5 seconds before we @@ -134,17 +136,16 @@ class RuleMap { } class UserCache { - constructor(config, ctx) { - this.timeout = config.cacheTimeout; + constructor(context, options) { + this.timeout = options.cacheTimeout; this.ruleMap = new RuleMap(); - this.userCfeeds = new Map(); this.newUserCfeed = (userId) => { let oldGroups = new Set(); const cfeed = new ReliableChangefeed( - r.table(config.usersTable).get(userId).changes({includeInitial: true}), - ctx.rdb_connection(), + r.table(options.usersTable).get(userId).changes({includeInitial: true}), + context.horizon.rdbConnection, { onUnready: () => { cfeed.unreadyAt = new Date(); @@ -176,8 +177,8 @@ class UserCache { this.queuedGroups = new Map(); this.groupsUnreadyAt = new Date(0); // epoch this.groupCfeed = new ReliableChangefeed( - r.table(config.groupsTable).changes({includeInitial: true}), - ctx.rdb_connection(), + r.table(options.groupsTable).changes({includeInitial: true}), + context.horizon.rdbConnection, { onReady: () => { this.ruleMap.delAllGroupRules(); @@ -227,6 +228,14 @@ class UserCache { ); } + close() { + let promises = [this.groupCfeed.close()]; + this.userCfeeds.forEach((feed) => { + promises.push(feed.close()); + }); + return Promise.all(promises); + } + subscribe(userId) { let cfeed = this.userCfeeds.get(userId); if (!cfeed) { @@ -250,10 +259,10 @@ class UserCache { if (userStale || groupsStale) { let staleSince = null; const curTime = Number(new Date()); - if (userStale && (curTime - Number(userStale) > staleMs)) { + if (userStale && (curTime - Number(userStale) > this.timeout)) { staleSince = userStale; } - if (groupsStale && (curTime - Number(groupsStale) > staleMs)) { + if (groupsStale && (curTime - Number(groupsStale) > this.timeout)) { if (!staleSince || Number(groupsStale) < Number(staleSince)) { staleSince = groupsStale; } @@ -307,74 +316,73 @@ class UserCache { } } -// RSI: remove the extra level of function calling. -module.exports = (raw_config) => { - // RSI: better default handling, use Joi? - const config = raw_config || { }; - if (config.cacheTimeout === undefined) { - config.cacheTimeout = 5000; - } - config.usersTable = config.usersTable || 'users'; - config.groupsTable = config.groupsTable || 'hz_groups'; - - const name = config.name; - const userCache = Symbol(`${name}_userCache`); - const userSub = Symbol(`${name}_userSub`); - let authCb, disconnectCb; - return { - name: 'hz_permissions', - - activate(ctx, onReady, onUnready) { - logger.info('Activating permissions plugin.'); - ctx[userCache] = new UserCache(config, ctx); - - authCb = (clientCtx) => { - clientCtx[userSub] = ctx[userCache].subscribe(clientCtx.user.id); - }; - ctx.on('auth', authCb); - - disconnectCb = (clientCtx) => { +const optionsSchema = Joi.object().keys({ + name: Joi.any().required(), + usersTable: Joi.string().default('users'), + groupsTable: Joi.string().default('hz_groups'), + cacheTimeout: Joi.number().positive().integer().default(5000), +}).unknown(true); + +module.exports = { + name: 'hz_permissions', + + activate(context, rawOptions, onReady, onUnready) { + const options = Joi.attempt(rawOptions, optionsSchema); + const userSub = Symbol(`${options.name}_userSub`); + + // Save things in the context that we will need at deactivation + const userCache = new UserCache(context, options); + context[options.name] = { + userCache, + authCb: (clientCtx) => { + clientCtx[userSub] = userCache.subscribe(clientCtx.user.id); + }, + disconnectCb: (clientCtx) => { if (clientCtx[userSub]) { clientCtx[userSub].close(); } - }; - ctx.on('disconnect', disconnectCb); - - return new Promise((resolve, reject) => { - ctx[userCache].groupCfeed.subscribe({onUnready, onReady: () => { - resolve({ - methods: { - hz_permissions: { - type: 'preReq', - handler: (req, res, next) => { - if (!req.clientCtx[userSub]) { - next(new Error('client connection not authenticated')); - } else { - req.clientCtx[userSub].getValidatePromise(req).then((validate) => { - req.setParameter(validate); - next(); - }).catch(next); - } - }, + }, + }; + + context.horizon.events.on('auth', context[options.name].authCb); + context.horizon.events.on('disconnect', context[options.name].disconnectCb); + + return new Promise((resolve, reject) => { + userCache.groupCfeed.subscribe({onUnready, onReady: () => { + resolve({ + methods: { + hz_permissions: { + type: 'prereq', + handler: (req, res, next) => { + if (!req.clientCtx[userSub]) { + next(new Error('Client connection is not authenticated.')); + } else { + req.clientCtx[userSub].getValidatePromise(req).then((validate) => { + req.setParameter(validate); + next(); + }).catch(next); + } }, }, - }); - onReady(); - }}); - }); + }, + }); + onReady(); + }}); + }); - }, + }, - deactivate(ctx) { - if (authCb) { - ctx.removeListener('auth', authCb); - } - if (disconnectCb) { - ctx.removeListener('disconnect', disconnectCb); - } - if (ctx[userCache]) { - ctx[userCache].close(); - } - }, - }; + deactivate(context, options) { + const pluginData = context[options.name]; + delete context[options.name]; + if (pluginData.authCb) { + context.removeListener('auth', pluginData.authCb); + } + if (pluginData.disconnectCb) { + context.removeListener('disconnect', pluginData.disconnectCb); + } + if (pluginData.userCache) { + pluginData.userCache.close(); + } + }, }; diff --git a/plugins/permissions/src/template.js b/plugins/permissions/src/template.js index 03ed3257a..3500e2c96 100644 --- a/plugins/permissions/src/template.js +++ b/plugins/permissions/src/template.js @@ -3,6 +3,7 @@ const assert = require('assert'); const vm = require('vm'); +// RSI: don't use the client AST - there are simple rules for generating options const ast = require('@horizon/client/lib/ast'); const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; const {remakeError} = require('@horizon/plugin-utils'); diff --git a/plugins/permit-all/package.json b/plugins/permit-all/package.json index b56289639..87a525b18 100644 --- a/plugins/permit-all/package.json +++ b/plugins/permit-all/package.json @@ -24,7 +24,7 @@ "dependencies": { }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/permit-all/src/permit-all.js b/plugins/permit-all/src/permit-all.js index 153cebd79..e5932c948 100644 --- a/plugins/permit-all/src/permit-all.js +++ b/plugins/permit-all/src/permit-all.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = () => ({ +module.exports = { name: 'hz_permissions', activate: () => ({ methods: { @@ -13,4 +13,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/remove/package.json b/plugins/remove/package.json index 76f50e90a..d5e120e2d 100644 --- a/plugins/remove/package.json +++ b/plugins/remove/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/remove/src/remove.js b/plugins/remove/src/remove.js index f6a4e3efe..fd048ec21 100644 --- a/plugins/remove/src/remove.js +++ b/plugins/remove/src/remove.js @@ -3,9 +3,9 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); -function remove(server) { +function remove(context) { return (request, response, next) => { - const conn = server.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -54,15 +54,15 @@ function remove(server) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_remove', - activate: (ctx) => ({ + activate: (context) => ({ methods: { remove: { type: 'terminal', requires: ['hz_permissions'], - handler: remove(ctx), + handler: remove(context), }, }, }), -}); +}; diff --git a/plugins/replace/package.json b/plugins/replace/package.json index 75649752d..62af70eb3 100644 --- a/plugins/replace/package.json +++ b/plugins/replace/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/replace/src/replace.js b/plugins/replace/src/replace.js index ad33276de..90bca6e35 100644 --- a/plugins/replace/src/replace.js +++ b/plugins/replace/src/replace.js @@ -3,9 +3,9 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); -function replace(server) { +function replace(context) { return (request, response, next) => { - const conn = server.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -45,15 +45,15 @@ function replace(server) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_replace', - activate: (ctx) => ({ + activate: (context) => ({ methods: { replace: { type: 'terminal', requires: ['hz_permissions'], - handler: replace(ctx), + handler: replace(context), }, }, }), -}); +}; diff --git a/plugins/store/package.json b/plugins/store/package.json index 441c2b5a6..8b431246d 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/store/src/store.js b/plugins/store/src/store.js index c6ed97685..7a4efabfd 100644 --- a/plugins/store/src/store.js +++ b/plugins/store/src/store.js @@ -3,9 +3,9 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); -function store(ctx) { +function store(context) { return (request, response, next) => { - const conn = ctx.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -58,15 +58,15 @@ function store(ctx) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_store', - activate: (ctx) => ({ + activate: (context) => ({ methods: { store: { type: 'terminal', requires: ['hz_permissions'], - handler: store(ctx), + handler: store(context), }, }, }), -}); +}; diff --git a/plugins/timeout/package.json b/plugins/timeout/package.json index f96f9d191..57d4c2e53 100644 --- a/plugins/timeout/package.json +++ b/plugins/timeout/package.json @@ -24,7 +24,7 @@ "dependencies": { }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/timeout/src/timeout.js b/plugins/timeout/src/timeout.js index 7a4275a5d..4aceeb951 100644 --- a/plugins/timeout/src/timeout.js +++ b/plugins/timeout/src/timeout.js @@ -12,7 +12,7 @@ function timeout(req, res, next) { } } -module.exports = () => ({ +module.exports = { name: 'hz_timeout', activate: () => ({ methods: { @@ -22,4 +22,4 @@ module.exports = () => ({ }, }, }), -}); +}; diff --git a/plugins/update/package.json b/plugins/update/package.json index f59ed998f..a3daf12ab 100644 --- a/plugins/update/package.json +++ b/plugins/update/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/update/src/update.js b/plugins/update/src/update.js index bac8896af..0c1e2fef3 100644 --- a/plugins/update/src/update.js +++ b/plugins/update/src/update.js @@ -3,9 +3,9 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); -function update(server) { +function update(context) { return (request, response, next) => { - const conn = server.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -50,15 +50,15 @@ function update(server) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_update', - activate: (ctx) => ({ + activate: (context) => ({ methods: { update: { type: 'terminal', requires: ['hz_permissions'], - handler: update(ctx), + handler: update(context), }, }, }), -}); +}; diff --git a/plugins/upsert/package.json b/plugins/upsert/package.json index df9bddf22..5c73710ef 100644 --- a/plugins/upsert/package.json +++ b/plugins/upsert/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/upsert/src/upsert.js b/plugins/upsert/src/upsert.js index b431b9fe2..f81353eed 100644 --- a/plugins/upsert/src/upsert.js +++ b/plugins/upsert/src/upsert.js @@ -3,9 +3,9 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); -function upsert(server) { +function upsert(context) { return (request, response, next) => { - const conn = server.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); const timeout = request.getParameter('timeout'); const collection = request.getParameter('collection'); const permissions = request.getParameter('hz_permissions'); @@ -65,15 +65,15 @@ function upsert(server) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_upsert', - activate: (ctx) => ({ + activate: (context) => ({ methods: { upsert: { type: 'terminal', requires: ['hz_permissions'], - handler: upsert(ctx), + handler: upsert(context), }, }, }), -}); +}; diff --git a/plugins/watch/package.json b/plugins/watch/package.json index 9d2fce4ac..362a403f6 100644 --- a/plugins/watch/package.json +++ b/plugins/watch/package.json @@ -25,7 +25,7 @@ "@horizon/plugin-utils": "1.0.0" }, "peerDependencies": { - "@horizon/plugin-router": "1.x", + "@horizon/plugin-router-base": "1.x", "@horizon/server": "3.x" }, "devDependencies": { diff --git a/plugins/watch/src/watch.js b/plugins/watch/src/watch.js index 2aeb42417..9acef04bd 100644 --- a/plugins/watch/src/watch.js +++ b/plugins/watch/src/watch.js @@ -2,11 +2,11 @@ const {reqlOptions, reads} = require('@horizon/plugin-utils'); -function watch(server) { +function watch(context) { return (req, res, next) => { const args = req.options.watch; const permissions = req.getParameter('hz_permissions'); - const conn = server.rdb_connection().connection(); + const conn = context.horizon.rdbConnection.connection(); if (args.length !== 0) { next(new Error(`"watch" expects 0 arguments but found ${args.length}`)); @@ -49,15 +49,15 @@ function watch(server) { }; } -module.exports = () => ({ +module.exports = { name: 'hz_watch', - activate: (ctx) => ({ + activate: (context) => ({ methods: { watch: { type: 'terminal', requires: ['hz_permissions'], - handler: watch(ctx), + handler: watch(context), }, }, }), -}); +}; diff --git a/server/package.json b/server/package.json index f1d17485a..7c57bc972 100644 --- a/server/package.json +++ b/server/package.json @@ -32,6 +32,7 @@ "oauth": "^0.9.14", "pem": "^1.8.1", "rethinkdb": "^2.1.1", + "toposort-class": "^1.0.1", "winston": "^2.1.0", "ws": "^1.1.0" }, diff --git a/server/src/auth.js b/server/src/auth.js index 23c3841bd..411b0ae90 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -97,7 +97,7 @@ class Auth { // TODO: maybe we should write something into the user data to track open sessions/tokens generate(provider, info) { return Promise.resolve().then(() => { - const conn = this._parent.rdb_connection().connection(); + const conn = this._parent.rdbConnection.connection(); const key = this.auth_key(provider, info); const db = r.db(this._parent.options.project_name); diff --git a/server/src/request.js b/server/src/request.js new file mode 100644 index 000000000..f6c2ff1f7 --- /dev/null +++ b/server/src/request.js @@ -0,0 +1,25 @@ +'use strict'; + +const assert = require('assert'); + +class Request { + constructor(request, currentMethod) { + this.request_id = request.request_id; + this.options = request.options; + this.clientCtx = request.clientCtx; + this._parameters = request._parameters; + this._currentMethod = currentMethod; + Object.freeze(this); + } + + getParameter(methodName) { + return this._parameters[methodName]; + } + + setParameter(value) { + assert(this._currentMethod); + return this._parameters[this._currentMethod] = value; + } +} + +module.exports = Request; diff --git a/server/src/schema/server_options.js b/server/src/schema/server_options.js index 3246d87a5..7aaea84ef 100644 --- a/server/src/schema/server_options.js +++ b/server/src/schema/server_options.js @@ -2,7 +2,7 @@ const Joi = require('joi'); -const server = Joi.object({ +const options = Joi.object({ project_name: Joi.string().default('horizon'), rdb_host: Joi.string().hostname().default('localhost'), rdb_port: Joi.number().greater(0).less(65536).default(28015), @@ -36,4 +36,4 @@ const method = Joi.object({ requires: Joi.array().single().items(Joi.string()).default([]), }).unknown(false); -module.exports = {server, auth, method}; +module.exports = {options, auth, method}; diff --git a/server/src/server.js b/server/src/server.js index 85d3cfa55..992b2d65a 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -4,7 +4,7 @@ const Auth = require('./auth').Auth; const ClientConnection = require('./client'); const logger = require('./logger'); const {ReliableConn, ReliableChangefeed} = require('./reliable'); -const {optionsSchema, methodSchema} = require('./schema/server_options'); +const schema = require('./schema/server_options'); const Request = require('./request'); const EventEmitter = require('events'); @@ -25,21 +25,20 @@ function handleProtocols(protocols, cb) { } } -class Server extends EventEmitter { +class Server { constructor(http_servers, user_opts) { - super(); - this.options = Joi.attempt(user_opts || { }, optionsSchema); + this.options = Joi.attempt(user_opts || { }, schema.options); this._auth_methods = { }; this._request_handlers = new Map(); this._ws_servers = []; this._close_promise = null; - this._defaultMiddlewareCb = (req, res, next) => { - next(new Error('No middleware to handle the request.')); - }; - this._middlewareCb = this._defaultMiddlewareCb; + this._methods = {}; + this._middlewareMethods = new Set(); this._auth = new Auth(this, this.options.auth); + this._clients = new Set(); + this.events = new EventEmitter(); - this._reliableConn = new ReliableConn({ + this.rdbConnection = new ReliableConn({ host: this.options.rdb_host, port: this.options.rdb_port, db: this.options.project_name, @@ -47,20 +46,13 @@ class Server extends EventEmitter { password: this.options.rdb_password || '', timeout: this.options.rdb_timeout || null, }); - this._clients = new Set(); - this.r = r; - this.logger = logger; - // RSI: better place to put this that on the context? Should plugins - // require the server? - this.ReliableChangefeed = ReliableChangefeed; - - this._clear_clients_subscription = this._reliableConn.subscribe({ + this._clear_clients_subscription = this.rdbConnection.subscribe({ onReady: () => { - this.emit('ready', this); + this.events.emit('ready', this); }, onUnready: (err) => { - this.emit('unready', this, err); + this.events.emit('unready', this, err); const msg = (err && err.message) || 'Connection became unready.'; this._clients.forEach((client) => client.close({error: msg})); this._clients.clear(); @@ -69,7 +61,7 @@ class Server extends EventEmitter { const verifyClient = (info, cb) => { // Reject connections if we aren't synced with the database - if (!this._reliableConn.ready) { + if (!this.rdbConnection.ready) { cb(false, 503, 'Connection to the database is down.'); } else { cb(true); @@ -85,7 +77,7 @@ class Server extends EventEmitter { .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => { try { - if (!this._reliableConn.ready) { + if (!this.rdbConnection.ready) { throw new Error('No connection to the database.'); } @@ -94,13 +86,13 @@ class Server extends EventEmitter { this._auth, this._getRequestHandler, this._getCapabilities, - this // Used to emit a client auth event + this.events, ); this._clients.add(client); - this.emit('connect', client.context()); - socket.on('close', () => { + this.events.emit('connect', client.context()); + socket.once('close', () => { this._clients.delete(client); - this.emit('disconnect', client.context()); + this.events.emit('disconnect', client.context()); }); } catch (err) { logger.error(`Failed to construct client: ${err}`); @@ -124,12 +116,8 @@ class Server extends EventEmitter { return this._auth; } - rdb_connection() { - return this._reliableConn; - } - - add_method(name, raw_options) { - const options = Joi.attempt(raw_options, optionsSchema); + addMethod(name, raw_options) { + const options = Joi.attempt(raw_options, schema.method); if (this._methods[name]) { throw new Error(`"${name}" is already registered as a method.`); } @@ -143,7 +131,7 @@ class Server extends EventEmitter { this._capabilities = null; } - remove_method(name) { + removeMethod(name) { delete this._methods[name]; this._middlewareMethods.delete(name); this._requirementsOrdering = null; @@ -254,7 +242,7 @@ class Server extends EventEmitter { this._close_promise = Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { s.close(resolve); - }))).then(() => this._reliableConn.close()); + }))).then(() => this.rdbConnection.close()); } return this._close_promise; } diff --git a/setupDev.sh b/setupDev.sh index 2500c9609..4ec58deff 100755 --- a/setupDev.sh +++ b/setupDev.sh @@ -38,7 +38,7 @@ link_dir () { link_dir client link_dir server "@horizon/client" link_dir cli "@horizon/server" -link_dir plugin-router +link_dir plugin-router/base link_dir plugin-utils # Link all the plugins - 'utils' must go first, and 'defaults' must go last diff --git a/test/package.json b/test/package.json index c90b0de69..9f000687a 100644 --- a/test/package.json +++ b/test/package.json @@ -5,7 +5,7 @@ "main": "dist/main.js", "scripts": { "lint": "eslint src test", - "test": "mocha dist/test", + "test": "mocha --timeout 10000 dist/test", "build": "babel src -d dist -s true" }, "repository": { diff --git a/test/src/serve.js b/test/src/serve.js index 815023b30..3bee55b55 100755 --- a/test/src/serve.js +++ b/test/src/serve.js @@ -6,7 +6,7 @@ require('../server/node_modules/source-map-support').install(); Error.stackTraceLimit = Infinity; const horizon_server = require('@horizon/server'); -const PluginRouter = require('@horizon/plugin-router'); +const PluginRouter = require('@horizon/plugin-router-base'); const plugins = require('@horizon/plugin-defaults'); // Utilities provided by the CLI library @@ -183,13 +183,12 @@ new Promise((resolve) => { permissions: 'permit-all', auto_create_collection: true, auto_create_index: true, - }).catch((err) => - console.log(`Plugin initialization failed: ${err.stack}`) - ); + }).catch((err) => { + console.log(`Plugin initialization failed: ${err.stack}`); + process.exit(1); + }); pluginRouter.once('ready', () => { - hz_server.set_middleware(pluginRouter.hzMiddleware()); - // Capture requests to `horizon.js` and `horizon.js.map` before the horizon server console.log('starting http servers'); http_servers.forEach((serv, i) => { diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 9468048be..2068391e2 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -1,7 +1,7 @@ 'use strict'; const horizon = require('@horizon/server'); -const PluginRouter = require('@horizon/plugin-router'); +const PluginRouter = require('@horizon/plugin-router-base'); const defaults = require('@horizon-plugins/defaults'); const logger = horizon.logger; @@ -23,7 +23,7 @@ logger.add(logger.transports.File, {filename: log_file}); logger.remove(logger.transports.Console); // Variables used by most tests -let rdb_server, rdb_http_port, rdb_port, rdb_conn, horizon_server, horizon_port, horizon_conn, horizon_listeners; +let rdb_server, rdb_http_port, rdb_port, rdb_conn, horizon_server, horizon_port, horizon_conn, horizon_listeners, plugin_router; let horizon_authenticated = false; const start_rethinkdb = () => { @@ -47,7 +47,7 @@ const start_rethinkdb = () => { }); }; -const stop_rethinkdb = () => rdb_server.close(); +const stop_rethinkdb = () => rdb_server && rdb_server.close(); // Used to prefix reql queries with the underlying table of a given collection const table = (collection) => @@ -144,18 +144,16 @@ const start_horizon_server = (done) => { }); plugin_router = new PluginRouter(horizon_server); - const plugins_promise = plugin_router.add(defaults({ + const plugins_promise = plugin_router.add(defaults, { auto_create_collection: true, auto_create_index: true, - })); - - horizon_server.set_middleware(plugin_router.hzMiddleware()); + }); - horizon_server.on('ready', () => { + horizon_server.events.on('ready', () => { logger.info('horizon server ready'); - plugins_promise.then(done).catch(done); + plugins_promise.then(() => done()).catch(done); }); - horizon_server.on('unready', (server, err) => { + horizon_server.events.on('unready', (server, err) => { logger.info(`horizon server unready: ${err}`); }); }); @@ -169,8 +167,8 @@ const close_horizon_server = () => { plugin_router = undefined; if (horizon_server !== undefined) { - horizon_server.removeAllListeners('ready'); - horizon_server.removeAllListeners('unready'); + horizon_server.events.removeAllListeners('ready'); + horizon_server.events.removeAllListeners('unready'); horizon_server.close(); } horizon_server = undefined; From 6fda0050e3602cfdf3f994e1a16e04ee8eeca09a Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 8 Sep 2016 20:37:03 -0700 Subject: [PATCH 76/86] got previously working tests working again --- plugins/defaults/src/defaults.js | 12 +++- server/src/reliable.js | 10 ++- server/src/server.js | 116 +++++++++++++++---------------- 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/plugins/defaults/src/defaults.js b/plugins/defaults/src/defaults.js index 82ce0c399..2d5cb3871 100644 --- a/plugins/defaults/src/defaults.js +++ b/plugins/defaults/src/defaults.js @@ -75,8 +75,10 @@ module.exports = { const promises = subplugins.map((plugin) => { const promise = Promise.resolve().then(() => + // Activate each plugin with their default name rather than the + // name of the defaults plugin plugin.activate(context, - options, + Object.assign({}, options, {name: plugin.name}), () => ready(plugin.name), () => unready(plugin.name)) ); @@ -94,8 +96,12 @@ module.exports = { deactivate: (context, options) => { const subplugins = context[options.name].subplugins; delete context[options.name]; - return Promise.all(subplugins.map((p) => - Promise.resolve().then(() => p.deactivate && p.deactivate(context, options)))); + return Promise.all(subplugins.map((plugin) => + Promise.resolve().then(() => { + if (plugin.deactivate) { + plugin.deactivate(context, Object.assign({}, options, {name: plugin.name})); + } + }))); }, }; diff --git a/server/src/reliable.js b/server/src/reliable.js index 66196e09e..0d077dc33 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -32,9 +32,8 @@ export class Reliable { try { cbs.onReady.apply(cbs, this.ready); } catch (e) { - // RSI: use logging facilities - console.error('Unexpected error in reliable callback, ' + - `event: subscribe onReady, error: ${e.stack}`); + logger.error('Unexpected error in reliable callback, ' + + `event: subscribe onReady, error: ${e.stack}`); } } return this[subs].get(subId); @@ -62,9 +61,8 @@ export class Reliable { event.apply(sub.cbs, args); } } catch (e) { - // RSI: use logging facilities - console.error('Unexpected error in reliable callback, ' + - `event: ${eventType}, error: ${e.stack}`); + logger.error('Unexpected error in reliable callback, ' + + `event: ${eventType}, error: ${e.stack}`); } }); } diff --git a/server/src/server.js b/server/src/server.js index 992b2d65a..1ad5cd9fe 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -59,6 +59,61 @@ class Server { }, }); + this._requestHandler = (req, res, next) => { + let terminal = null; + const requirements = {}; + + this._middlewareMethods.forEach((name) => { + requirements[name] = true; + }); + + for (const o in req.options) { + const m = this._methods[o]; + if (!m) { + next(new Error(`No method to handle option "${o}".`)); + return; + } + + if (m.type === 'terminal') { + if (terminal !== null) { + next(new Error('Multiple terminal methods in request: ' + + `"${terminal}", "${o}".`)); + return; + } + terminal = o; + } else { + requirements[o] = true; + } + for (const r of m.requires) { + requirements[r] = true; + } + } + + if (terminal === null) { + next(new Error('No terminal method was specified in the request.')); + } else if (requirements[terminal]) { + next(new Error(`Terminal method "${terminal}" is also a requirement.`)); + } else { + const ordering = this._getRequirementsOrdering(); + const chain = Object.keys(requirements).sort( + (a, b) => ordering[a] - ordering[b]); + chain.push(terminal); + + chain.reduceRight((cb, methodName) => + (maybeErr) => { + if (maybeErr instanceof Error) { + next(maybeErr); + } else { + try { + this._methods[methodName].handler(new Request(req, methodName), res, cb); + } catch (e) { + next(e); + } + } + }, next)(); + } + }; + const verifyClient = (info, cb) => { // Reject connections if we aren't synced with the database if (!this.rdbConnection.ready) { @@ -84,7 +139,7 @@ class Server { const client = new ClientConnection( socket, this._auth, - this._getRequestHandler, + this._requestHandler, this._getCapabilities, this.events, ); @@ -121,7 +176,7 @@ class Server { if (this._methods[name]) { throw new Error(`"${name}" is already registered as a method.`); } - this._methods[name] = raw_options; + this._methods[name] = options; if (options.type === 'middleware') { this._middlewareMethods.add(name); @@ -179,63 +234,6 @@ class Server { return this._capabilities; } - _getRequestHandler() { - return (req, res, next) => { - let terminal = null; - const requirements = {}; - - this._middlewareMethods.forEach((name) => { - requirements[name] = true; - }); - - for (const o in req.options) { - const m = this._methods[o]; - if (!m) { - next(new Error(`No method to handle option "${o}".`)); - return; - } - - if (m.type === 'terminal') { - if (terminal !== null) { - next(new Error('Multiple terminal methods in request: ' + - `"${terminal}", "${o}".`)); - return; - } - terminal = o; - } else { - requirements[o] = true; - } - for (const r of m.requires) { - requirements[r] = true; - } - } - - if (terminal === null) { - next(new Error('No terminal method was specified in the request.')); - } else if (requirements[terminal]) { - next(new Error(`Terminal method "${terminal}" is also a requirement.`)); - } else { - const ordering = this.requirementsOrdering(); - const chain = Object.keys(requirements).sort( - (a, b) => ordering[a] - ordering[b]); - chain.push(terminal); - - chain.reduceRight((cb, methodName) => - (maybeErr) => { - if (maybeErr instanceof Error) { - next(maybeErr); - } else { - try { - this._methods[methodName].handler(new Request(req, methodName), res, cb); - } catch (e) { - next(e); - } - } - }, next)(); - } - }; - } - // TODO: We close clients in `onUnready` above, but don't wait for them to be closed. close() { if (!this._close_promise) { From 8079784c007ac79a0febcf0649a65158afc8d2fb Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 8 Sep 2016 22:04:51 -0700 Subject: [PATCH 77/86] fixing problems in integration tests --- cli/src/utils/start_rdb_server.js | 5 + plugin-router/base/src/index.js | 6 +- plugin-utils/src/utils.js | 3 - plugin-utils/src/writes.js | 7 +- plugins/collection/src/collection.js | 7 +- plugins/remove/src/remove.js | 3 +- plugins/replace/src/replace.js | 3 +- plugins/store/src/store.js | 3 +- plugins/update/src/update.js | 3 +- plugins/upsert/src/upsert.js | 3 +- test/src/test/test.js | 21 ++--- test/src/test/utils.js | 131 ++++++++++++++------------- test/src/test/write.js | 6 +- 13 files changed, 109 insertions(+), 92 deletions(-) diff --git a/cli/src/utils/start_rdb_server.js b/cli/src/utils/start_rdb_server.js index e665d186c..5d1cab057 100644 --- a/cli/src/utils/start_rdb_server.js +++ b/cli/src/utils/start_rdb_server.js @@ -107,17 +107,22 @@ class RethinkdbServer { } close() { + console.log('closing rdb server'); return new Promise((resolve) => { if (this.proc.exitCode !== null) { + console.log('rdb server already exited'); resolve(); } else { + console.log('killing server with sigterm'); this.proc.kill('SIGTERM'); this.proc.once('exit', () => { + console.log('rdb server exited'); resolve(); }); setTimeout(() => { + console.log('killing server with sigkill'); this.proc.kill('SIGKILL'); resolve(); }, 20000).unref(); diff --git a/plugin-router/base/src/index.js b/plugin-router/base/src/index.js index 3d0419f21..dc8db1714 100644 --- a/plugin-router/base/src/index.js +++ b/plugin-router/base/src/index.js @@ -19,6 +19,7 @@ class PluginRouter extends EventEmitter { } close() { + console.log(`closing plugin-router: ${JSON.stringify(this.plugins.keys())}`); return Promise.all(Array.from(this.plugins.keys()).map((p) => this.remove(p))); } @@ -71,14 +72,17 @@ class PluginRouter extends EventEmitter { } this.plugins.delete(name); + console.log(`deactivating plugin ${name}`); return plugin.activatePromise.then((active) => { for (const m in active.methods) { this.horizon.removeMethod(m); } if (plugin.deactivate) { return plugin.deactivate(this.context, plugin.options, - reason || 'Removed from PluginRouter.'); + reason || 'Removed from PluginRouter.').then(() => + console.log(`plugin ${name} deactivated, removing methods`)); } + console.log(`plugin ${name} has no deactivate, done`); }); } diff --git a/plugin-utils/src/utils.js b/plugin-utils/src/utils.js index f97d0741f..52d8debd7 100644 --- a/plugin-utils/src/utils.js +++ b/plugin-utils/src/utils.js @@ -51,14 +51,11 @@ const reqlOptions = { binaryFormat: 'raw', }; -const versionField = '$hz_v$'; - module.exports = { rethinkdbVersionCheck, remakeError, isObject, reqlOptions, - versionField, reads: require('./reads'), writes: require('./writes'), test: require('./test'), diff --git a/plugin-utils/src/writes.js b/plugin-utils/src/writes.js index 9d7f97b68..a7f6353c0 100644 --- a/plugin-utils/src/writes.js +++ b/plugin-utils/src/writes.js @@ -1,12 +1,10 @@ 'use strict'; -const utils = require('./utils'); - const assert = require('assert'); const {r} = require('@horizon/server'); -const hz_v = utils.version_field; +const hz_v = '$hz_v$'; // Common functionality used by write requests const invalidated_msg = 'Write invalidated by another request, try again.'; @@ -15,7 +13,7 @@ const timeout_msg = 'Operation timed out.'; const unauthorized_msg = 'Operation not permitted.'; function apply_version(row, new_version) { - row.merge(r.object(hz_v, new_version)); + return row.merge(r.object(hz_v, new_version)); } function make_write_response(data) { @@ -189,4 +187,5 @@ module.exports = { retry_loop, validate_old_row_required, validate_old_row_optional, + versionField: hz_v, }; diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 09986bfcb..e6e014d93 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -63,8 +63,11 @@ module.exports = { }); }, deactivate: (context, options) => { - if (context[options.name]) { - context[options.name].close(); + console.log('deactivating collections plugin'); + const metadata = context[options.name]; + delete context[options.name]; + if (metadata) { + metadata.close(); } }, }; diff --git a/plugins/remove/src/remove.js b/plugins/remove/src/remove.js index fd048ec21..33e6ed9cb 100644 --- a/plugins/remove/src/remove.js +++ b/plugins/remove/src/remove.js @@ -1,7 +1,8 @@ 'use strict'; const {r} = require('@horizon/server'); -const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); +const {reqlOptions, writes} = require('@horizon/plugin-utils'); +const hz_v = writes.versionField; function remove(context) { return (request, response, next) => { diff --git a/plugins/replace/src/replace.js b/plugins/replace/src/replace.js index 90bca6e35..5223f153f 100644 --- a/plugins/replace/src/replace.js +++ b/plugins/replace/src/replace.js @@ -1,7 +1,8 @@ 'use strict'; const {r} = require('@horizon/server'); -const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); +const {reqlOptions, writes} = require('@horizon/plugin-utils'); +const hz_v = writes.versionField; function replace(context) { return (request, response, next) => { diff --git a/plugins/store/src/store.js b/plugins/store/src/store.js index 7a4efabfd..520cf48e9 100644 --- a/plugins/store/src/store.js +++ b/plugins/store/src/store.js @@ -1,7 +1,8 @@ 'use strict'; const {r} = require('@horizon/server'); -const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); +const {reqlOptions, writes} = require('@horizon/plugin-utils'); +const hz_v = writes.versionField; function store(context) { return (request, response, next) => { diff --git a/plugins/update/src/update.js b/plugins/update/src/update.js index 0c1e2fef3..b82e242e7 100644 --- a/plugins/update/src/update.js +++ b/plugins/update/src/update.js @@ -1,7 +1,8 @@ 'use strict'; const {r} = require('@horizon/server'); -const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); +const {reqlOptions, writes} = require('@horizon/plugin-utils'); +const hz_v = writes.versionField; function update(context) { return (request, response, next) => { diff --git a/plugins/upsert/src/upsert.js b/plugins/upsert/src/upsert.js index f81353eed..82610c574 100644 --- a/plugins/upsert/src/upsert.js +++ b/plugins/upsert/src/upsert.js @@ -1,7 +1,8 @@ 'use strict'; const {r} = require('@horizon/server'); -const {reqlOptions, writes, versionField: hz_v} = require('@horizon/plugin-utils'); +const {reqlOptions, writes} = require('@horizon/plugin-utils'); +const hz_v = writes.versionField; function upsert(context) { return (request, response, next) => { diff --git a/test/src/test/test.js b/test/src/test/test.js index 8a7651859..783e927e3 100644 --- a/test/src/test/test.js +++ b/test/src/test/test.js @@ -12,10 +12,14 @@ const all_suites = ['prereq', 'watch', 'write', 'permissions']; + const collection = 'test'; -before('Start RethinkDB Server', () => utils.start_rethinkdb()); -after('Stop RethinkDB Server', () => utils.stop_rethinkdb()); +before('Start servers', () => utils.startServers()); +after('Stop servers', () => utils.stopServers()); + +before(`Creating general-purpose collection: '${collection}'`, + () => utils.create_collection(collection)); beforeEach( /** @this mocha */ @@ -25,14 +29,7 @@ afterEach( /** @this mocha */ function() { logger.info(`End test '${this.currentTest.title}'`); }); -describe('Horizon Server', () => { - before('Start Horizon Server', utils.start_horizon_server); - after('Close Horizon Server', utils.close_horizon_server); - - before(`Creating general-purpose collection: '${collection}'`, - () => utils.create_collection(collection)); +beforeEach('Connect Horizon Client', utils.open_horizon_conn); +afterEach('Close Horizon Client', utils.close_horizon_conn); - beforeEach('Connect Horizon Client', utils.open_horizon_conn); - afterEach('Close Horizon Client', utils.close_horizon_conn); - all_suites.forEach((s) => require(`./${s}`).suite(collection)); -}); +all_suites.forEach((s) => require(`./${s}`).suite(collection)); diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 2068391e2..7a6b69940 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -15,7 +15,7 @@ const r = require('rethinkdb'); const websocket = require('ws'); const project_name = 'integration_test'; -const data_dir = './rethinkdb_data_test'; +const dataDir = './rethinkdb_data_test'; const log_file = `./horizon_test_${process.pid}.log`; logger.level = 'debug'; @@ -23,15 +23,18 @@ logger.add(logger.transports.File, {filename: log_file}); logger.remove(logger.transports.Console); // Variables used by most tests -let rdb_server, rdb_http_port, rdb_port, rdb_conn, horizon_server, horizon_port, horizon_conn, horizon_listeners, plugin_router; +let rdb_server, rdb_http_port, rdb_port, rdb_conn, horizon_server, horizon_port, horizon_conn, horizon_listeners, plugin_router, http_server; let horizon_authenticated = false; -const start_rethinkdb = () => { - logger.info('removing dir'); - rm_sync_recursive(data_dir); +function startServers() { + assert.strictEqual(horizon_server, undefined); + assert.strictEqual(plugin_router, undefined); + + logger.info(`removing old rethinkdb data directory: ${dataDir}`); + rm_sync_recursive(dataDir); logger.info('creating server'); - return start_rdb_server({dataDir: data_dir}).then((server) => { + return start_rdb_server({dataDir}).then((server) => { rdb_server = server; rdb_port = server.driver_port; rdb_http_port = server.http_port; @@ -41,13 +44,66 @@ const start_rethinkdb = () => { }).then((conn) => { logger.info('connected'); rdb_conn = conn; - return r.dbCreate(project_name).run(conn); - }).then((res) => { - assert.strictEqual(res.dbs_created, 1); + }).then(() => { + logger.info('creating http server'); + + http_server = new http.Server(); + return new Promise((resolve, reject) => { + http_server.listen(0, () => { + logger.info('creating horizon server'); + horizon_port = http_server.address().port; + horizon_server = new horizon.Server(http_server, { + project_name, + rdb_port, + auth: { + token_secret: 'hunter2', + allow_unauthenticated: true, + }, + }); + + plugin_router = new PluginRouter(horizon_server); + const plugins_promise = plugin_router.add(defaults, { + auto_create_collection: true, + auto_create_index: true, + }); + + horizon_server.events.on('ready', () => { + logger.info('horizon server ready'); + plugins_promise.then(resolve).catch(reject); + }); + horizon_server.events.on('unready', (server, err) => { + logger.info(`horizon server unready: ${err}`); + }); + }); + http_server.on('error', reject); + }); }); -}; +} + +function stopServers() { + let localRdbServer = rdb_server; + let localPluginRouter = plugin_router; + let localHorizonServer = horizon_server; + plugin_router = undefined; + horizon_server = undefined; + rdb_server = undefined; -const stop_rethinkdb = () => rdb_server && rdb_server.close(); + return Promise.resolve().then(() => { + if (localPluginRouter) { + return localPluginRouter.close(); + } + }).then(() => { + if (localHorizonServer) { + localHorizonServer.events.removeAllListeners('ready'); + localHorizonServer.events.removeAllListeners('unready'); + return localHorizonServer.close(); + } + }).then(() => { + if (localRdbServer) { + localRdbServer.close(); + } + }); +} // Used to prefix reql queries with the underlying table of a given collection const table = (collection) => @@ -125,55 +181,6 @@ const populate_collection = (collection, rows) => { } }; -const start_horizon_server = (done) => { - logger.info('creating http server'); - assert.strictEqual(horizon_server, undefined); - assert.strictEqual(plugin_router, undefined); - - const http_server = new http.Server(); - http_server.listen(0, () => { - logger.info('creating horizon server'); - horizon_port = http_server.address().port; - horizon_server = new horizon.Server(http_server, { - project_name, - rdb_port, - auth: { - token_secret: 'hunter2', - allow_unauthenticated: true, - }, - }); - - plugin_router = new PluginRouter(horizon_server); - const plugins_promise = plugin_router.add(defaults, { - auto_create_collection: true, - auto_create_index: true, - }); - - horizon_server.events.on('ready', () => { - logger.info('horizon server ready'); - plugins_promise.then(() => done()).catch(done); - }); - horizon_server.events.on('unready', (server, err) => { - logger.info(`horizon server unready: ${err}`); - }); - }); - http_server.on('error', (err) => done(err)); -}; - -const close_horizon_server = () => { - if (plugin_router !== undefined) { - plugin_router.close(); - } - plugin_router = undefined; - - if (horizon_server !== undefined) { - horizon_server.events.removeAllListeners('ready'); - horizon_server.events.removeAllListeners('unready'); - horizon_server.close(); - } - horizon_server = undefined; -}; - const add_horizon_listener = (request_id, cb) => { assert(horizon_authenticated, 'horizon_conn was not authenticated before making requests'); assert.notStrictEqual(request_id, undefined); @@ -306,12 +313,12 @@ module.exports = { horizon_port: () => horizon_port, horizon_listeners: () => horizon_listeners, - start_rethinkdb, stop_rethinkdb, + startServers, stopServers, + create_collection, populate_collection, clear_collection, - start_horizon_server, close_horizon_server, open_horizon_conn, close_horizon_conn, horizon_auth, horizon_admin_auth, horizon_default_auth, add_horizon_listener, remove_horizon_listener, diff --git a/test/src/test/write.js b/test/src/test/write.js index 01b54a311..4d36344ce 100644 --- a/test/src/test/write.js +++ b/test/src/test/write.js @@ -6,7 +6,7 @@ const pluginUtils = require('@horizon/plugin-utils'); const assert = require('assert'); const crypto = require('crypto'); -const hz_v = pluginUtils.version_field; +const hz_v = pluginUtils.writes.versionField; const invalidated_msg = pluginUtils.writes.invalidated_msg; // Before each test, ids [0, 4) will be present in the collection @@ -48,7 +48,7 @@ const all_tests = (collection) => { const make_request = (type, data, options) => ({ request_id: crypto.randomBytes(4).readUInt32BE(), - options: Object.assign({type: []}, options || {}, {collection, data}), + options: Object.assign({}, options || {}, {collection: [collection], [type]: data}), }); const check_collection = (expected, done) => { @@ -571,7 +571,7 @@ const all_tests = (collection) => { // per iteration with the database. In order to test timeouts, we use a // timeout of zero, so the other rows should immediately error. describe('Zero Timeout', () => { - const timeout = {timeout: 0}; + const timeout = {timeout: [0]}; const test_data = [{id: 0, value: 0}]; beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); From 93617159d3662003e2533813e05b561128e13c88 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 8 Sep 2016 22:10:03 -0700 Subject: [PATCH 78/86] fixed cleanup, removed old console log statements --- cli/src/utils/start_rdb_server.js | 5 ----- plugin-router/base/src/index.js | 18 +++++++----------- plugins/collection/src/collection.js | 1 - plugins/permissions/src/permissions.js | 4 ++-- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/cli/src/utils/start_rdb_server.js b/cli/src/utils/start_rdb_server.js index 5d1cab057..e665d186c 100644 --- a/cli/src/utils/start_rdb_server.js +++ b/cli/src/utils/start_rdb_server.js @@ -107,22 +107,17 @@ class RethinkdbServer { } close() { - console.log('closing rdb server'); return new Promise((resolve) => { if (this.proc.exitCode !== null) { - console.log('rdb server already exited'); resolve(); } else { - console.log('killing server with sigterm'); this.proc.kill('SIGTERM'); this.proc.once('exit', () => { - console.log('rdb server exited'); resolve(); }); setTimeout(() => { - console.log('killing server with sigkill'); this.proc.kill('SIGKILL'); resolve(); }, 20000).unref(); diff --git a/plugin-router/base/src/index.js b/plugin-router/base/src/index.js index dc8db1714..1b9fd6b20 100644 --- a/plugin-router/base/src/index.js +++ b/plugin-router/base/src/index.js @@ -19,7 +19,6 @@ class PluginRouter extends EventEmitter { } close() { - console.log(`closing plugin-router: ${JSON.stringify(this.plugins.keys())}`); return Promise.all(Array.from(this.plugins.keys()).map((p) => this.remove(p))); } @@ -60,29 +59,26 @@ class PluginRouter extends EventEmitter { activatePromise.then(() => this._noteReady(options.name)); } - this.plugins.set(options.name, {options, activatePromise}); + this.plugins.set(options.name, {options, activatePromise, plugin}); return activatePromise; } remove(name, reason) { - const plugin = this.plugins.get(name); + const info = this.plugins.get(name); - if (!plugin) { + if (!info) { return Promise.reject(new Error(`Plugin "${name}" is not present.`)); } this.plugins.delete(name); - console.log(`deactivating plugin ${name}`); - return plugin.activatePromise.then((active) => { + return info.activatePromise.then((active) => { for (const m in active.methods) { this.horizon.removeMethod(m); } - if (plugin.deactivate) { - return plugin.deactivate(this.context, plugin.options, - reason || 'Removed from PluginRouter.').then(() => - console.log(`plugin ${name} deactivated, removing methods`)); + if (info.plugin.deactivate) { + return info.plugin.deactivate(this.context, info.options, + reason || 'Removed from PluginRouter.'); } - console.log(`plugin ${name} has no deactivate, done`); }); } diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index e6e014d93..5a88cd602 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -63,7 +63,6 @@ module.exports = { }); }, deactivate: (context, options) => { - console.log('deactivating collections plugin'); const metadata = context[options.name]; delete context[options.name]; if (metadata) { diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 02fedb7a6..59926390c 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -376,10 +376,10 @@ module.exports = { const pluginData = context[options.name]; delete context[options.name]; if (pluginData.authCb) { - context.removeListener('auth', pluginData.authCb); + context.horizon.events.removeListener('auth', pluginData.authCb); } if (pluginData.disconnectCb) { - context.removeListener('disconnect', pluginData.disconnectCb); + context.horizon.events.removeListener('disconnect', pluginData.disconnectCb); } if (pluginData.userCache) { pluginData.userCache.close(); From 06d1427493eb30f0b23fe253e93aba04c2533f52 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Fri, 9 Sep 2016 00:55:44 -0700 Subject: [PATCH 79/86] fixed some tests --- plugin-utils/src/writes.js | 2 +- plugins/collection/src/types/metadata.js | 4 +- plugins/permissions/src/permissions.js | 20 ++++- plugins/permissions/src/rule.js | 4 +- plugins/permissions/src/template.js | 2 +- plugins/timeout/src/timeout.js | 2 +- test/src/test/prereq.js | 96 ++++++++++++------------ 7 files changed, 72 insertions(+), 58 deletions(-) diff --git a/plugin-utils/src/writes.js b/plugin-utils/src/writes.js index a7f6353c0..1ed9791f2 100644 --- a/plugin-utils/src/writes.js +++ b/plugin-utils/src/writes.js @@ -22,7 +22,7 @@ function make_write_response(data) { data[index] = {error: item.message}; } }); - return {data, state: 'complete'}; + return data; } // This function returns a Promise that resolves to an array of responses - diff --git a/plugins/collection/src/types/metadata.js b/plugins/collection/src/types/metadata.js index 59b8bde1f..291590370 100644 --- a/plugins/collection/src/types/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -296,9 +296,9 @@ class ReliableMetadata extends Reliable { return new Promise((resolve, reject) => collection._on_ready((maybeErr) => { if (maybeErr instanceof Error) { - resolve(collection); - } else { reject(maybeErr); + } else { + resolve(collection); } })); } diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 59926390c..3fac49574 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -6,7 +6,7 @@ const assert = require('assert'); const Joi = require('joi'); -const {r, logger, ReliableChangefeed} = require('@horizon/server'); +const {r, logger, Reliable, ReliableChangefeed} = require('@horizon/server'); // We can be desynced from the database for up to 5 seconds before we // start rejecting queries. @@ -174,6 +174,14 @@ class UserCache { return cfeed; }; + // Set up a dummy user cfeed for unauthenticated users (null id) + this.ruleMap.addUserGroup(null, 'default'); + this.userCfeeds.set(null, Object.assign(new Reliable(), { + refcount: 1, + unreadyAt: null, + readyPromise: Promise.resolve() + })); + this.queuedGroups = new Map(); this.groupsUnreadyAt = new Date(0); // epoch this.groupCfeed = new ReliableChangefeed( @@ -220,7 +228,13 @@ class UserCache { } } for (const k in newRules) { - this.ruleMap.addGroupRule(id, k, new Rule(newRules[k])); + try { + this.ruleMap.addGroupRule(id, k, new Rule(newRules[k])); + } catch (err) { + logger.error(`Failed to evaluate rule ${id}.${k}: ${err}`); + logger.debug(`Contents: ${JSON.stringify(newRules[k])}`); + logger.debug(`Stack: ${err.stack}`); + } } } }, @@ -240,6 +254,7 @@ class UserCache { let cfeed = this.userCfeeds.get(userId); if (!cfeed) { this.userCfeeds.set(userId, cfeed = this.newUserCfeed(userId)); + // RSI: move this into newUserCfeed? cfeed.readyPromise = new Promise((resolve, reject) => { cfeed.subscribe({onReady: () => resolve()}); setTimeout(() => reject(new Error('timed out')), this.timeout); @@ -357,6 +372,7 @@ module.exports = { if (!req.clientCtx[userSub]) { next(new Error('Client connection is not authenticated.')); } else { + // RSI: test timeout behavior - anecdotal evidence points to 'broken' req.clientCtx[userSub].getValidatePromise(req).then((validate) => { req.setParameter(validate); next(); diff --git a/plugins/permissions/src/rule.js b/plugins/permissions/src/rule.js index 7e6399bcc..10deee839 100644 --- a/plugins/permissions/src/rule.js +++ b/plugins/permissions/src/rule.js @@ -1,7 +1,7 @@ 'use strict'; -const Template = require('./template').Template; -const Validator = require('./validator').Validator; +const Template = require('./template'); +const Validator = require('./validator'); class Rule { constructor(info) { diff --git a/plugins/permissions/src/template.js b/plugins/permissions/src/template.js index 3500e2c96..cc5daab0e 100644 --- a/plugins/permissions/src/template.js +++ b/plugins/permissions/src/template.js @@ -246,4 +246,4 @@ class Template { } } -module.exports = {Template}; +module.exports = Template; diff --git a/plugins/timeout/src/timeout.js b/plugins/timeout/src/timeout.js index 4aceeb951..495a5c204 100644 --- a/plugins/timeout/src/timeout.js +++ b/plugins/timeout/src/timeout.js @@ -7,7 +7,7 @@ function timeout(req, res, next) { } else if (typeof args[0] !== 'number') { next(new Error('timeout must be a number')); } else { - req.setParameter(new Date() + args[0]); + req.setParameter(new Date(new Date().getTime() + args[0])); next(); } } diff --git a/test/src/test/prereq.js b/test/src/test/prereq.js index c6ee27a0a..06d3c649c 100644 --- a/test/src/test/prereq.js +++ b/test/src/test/prereq.js @@ -11,57 +11,55 @@ const all_tests = (collection) => { // Launch simultaneous queries that depend on a non-existent collection, then // verify that only one table exists for that collection. - it('collection create race on read', - /** @this mocha */ - function(done) { - const query_count = 5; - const rand_collection = crypto.randomBytes(8).toString('hex'); + it('collection create race on read', (done) => { + const query_count = 5; + const rand_collection = crypto.randomBytes(8).toString('hex'); - let finished = 0; - for (let i = 0; i < query_count; ++i) { - utils.stream_test( - {request_id: i, options: {collection: rand_collection, query: []}}, - (err, res) => { - assert.ifError(err); - assert.strictEqual(res.length, 0); - if (++finished === query_count) { - utils.table(rand_collection).count().run(utils.rdb_conn()) - .then((count) => (assert.strictEqual(count, 0), done()), - (error) => done(error)); - } - }); - } - }); + let finished = 0; + for (let i = 0; i < query_count; ++i) { + utils.stream_test( + {request_id: i, options: {collection: [rand_collection], fetch: []}}, + (err, res) => { + assert.ifError(err); + assert.strictEqual(res.length, 0); + if (++finished === query_count) { + utils.table(rand_collection).count().run(utils.rdb_conn()).then((count) => { + assert.strictEqual(count, 0); + done(); + }).catch(done); + } + }); + } + }); // Same as the previous test, but it exists because the ReQL error message // is different for a read or a write when the table is unavailable. - it('collection create race on write', - /** @this mocha */ - function(done) { - const query_count = 5; - const rand_collection = crypto.randomBytes(8).toString('hex'); + it('collection create race on write', (done) => { + const query_count = 5; + const rand_collection = crypto.randomBytes(8).toString('hex'); - let finished = 0; - for (let i = 0; i < query_count; ++i) { - utils.stream_test( - { - request_id: i, - options: { - collection: rand_collection, - insert: [{ }], - }, - }, - (err, res) => { - assert.ifError(err); - assert.strictEqual(res.length, 1); - if (++finished === query_count) { - utils.table(rand_collection).count().run(utils.rdb_conn()) - .then((count) => (assert.strictEqual(count, query_count), done()), - (error) => done(error)); - } - }); - } - }); + let finished = 0; + for (let i = 0; i < query_count; ++i) { + utils.stream_test( + { + request_id: i, + options: { + collection: [rand_collection], + insert: [{}], + }, + }, + (err, res) => { + assert.ifError(err); + assert.strictEqual(res.length, 1); + if (++finished === query_count) { + utils.table(rand_collection).count().run(utils.rdb_conn()).then((count) => { + assert.strictEqual(count, query_count); + done(); + }).catch(done); + } + }); + } + }); // Launch two simultaneous queries that depend on a non-existent index, then // verify that only one such index exists with that name. @@ -77,9 +75,9 @@ const all_tests = (collection) => { { request_id: i, options: { - collection, + collection: [collection], order: [[field_name], 'ascending'], - query: [], + fetch: [], }, }, (err, res) => { @@ -89,7 +87,7 @@ const all_tests = (collection) => { utils.table(collection).indexStatus().count().run(conn).then((new_count) => { assert.strictEqual(old_count + 1, new_count); done(); - }, (err2) => done(err2)); + }).catch(done); } }); } From 0392e909fd4d8f9195b8eb41884716b485e1da1e Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sat, 10 Sep 2016 06:55:52 -0700 Subject: [PATCH 80/86] almost have all server tests running --- .gitignore | 1 + plugin-utils/src/reads.js | 261 ++++++++++++++------- plugins/above/src/above.js | 22 +- plugins/below/src/below.js | 22 +- plugins/collection/src/types/collection.js | 12 +- plugins/fetch/src/fetch.js | 6 +- plugins/find/src/find.js | 7 + plugins/findAll/src/findAll.js | 2 + plugins/order/src/order.js | 4 +- test/src/test/fetch.js | 18 ++ test/src/test/permissions.js | 40 ++-- 11 files changed, 267 insertions(+), 128 deletions(-) diff --git a/.gitignore b/.gitignore index af18e5e1f..85740e6b7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ node_modules/ .hz/ config.toml **/.vscode +**/.*.swp **/dist/ diff --git a/plugin-utils/src/reads.js b/plugin-utils/src/reads.js index 3aedb4896..90b7d58b7 100644 --- a/plugin-utils/src/reads.js +++ b/plugin-utils/src/reads.js @@ -1,8 +1,11 @@ 'use strict'; +const assert = require('assert'); + const {r} = require('@horizon/server'); -function object_to_fields(obj) { +// For a given object, returns the array of fields present +function objectToFields(obj) { return Object.keys(obj).map((key) => { const value = obj[key]; if (value !== null && typeof value === 'object' && !value.$reql_type$) { @@ -13,111 +16,189 @@ function object_to_fields(obj) { }); } -// This is exposed to be reused by 'subscribe' -const makeReadReql = (req) => Promise.resolve().then(() => { - const find = req.getParameter('find'); - const limit = req.getParameter('limit'); - const order = req.getParameter('order'); - const above = req.getParameter('above'); - const below = req.getParameter('below'); - const findAll = req.getParameter('findAll'); - const collection = req.getParameter('collection'); - - if (!collection) { - throw new Error('"collection" was not specified.'); - } else if (find && findAll) { - throw new Error('Cannot specify both "find" and "findAll".'); - } else if (find && (limit || order || above || below)) { - throw new Error('Cannot specify "find" with "limit", "order", "above", or "below".'); - } else if ((above || below) && !order) { - throw new Error('Cannot specify "above" or "below" without "order".'); +// Gets the value of a field out of an object, or undefined if it is not present +function getObjectField(obj, field) { + let value = obj; + for (const name of field) { + if (value === undefined) { + return value; + } + value = value[name]; } + return value; +} - const order_keys = (order && order[0]) || []; - let aboveKeyCount = above ? Object.keys(above[0]).length : 0; - let belowKeyCount = below ? Object.keys(below[0]).length : 0; - order_keys.forEach((k) => { - if (above) { - if (above[0][k] !== undefined) { - aboveKeyCount -= 1; - } else if (aboveKeyCount !== 0) { - throw new Error('The keys in "above" must appear continguously ' + - 'from the start of "order".'); - } +// Gets the value of a field out of an object, throws an error if it is not present +function guaranteeObjectField(obj, field) { + const res = getObjectField(obj, field); + assert(res !== undefined); + return res; +} + +// Compares two fields, returns true if they are identical, false otherwise +function isSameField(a, b) { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < b.length; ++i) { + if (a[i] !== b[i]) { + return false; } - if (below) { - if (below[0][k] !== undefined) { - belowKeyCount -= 1; - } else if (belowKeyCount !== 0) { - throw new Error('The keys in "below" must appear continguously ' + - 'from the start of "order".'); - } + } + return true; +} + +// Returns true if the expected field is in the array of fields +function hasField(fields, expected) { + for (let i = 0; i < fields.length; ++i) { + if (isSameField(fields[i], expected)) { + return true; } + } +} + +function makeFindReql(collection, find) { + return collection.get_matching_index(objectToFields(find), []).then((index) => { + let value = index.fields.map((field) => guaranteeObjectField(find, field)); + + if (index.name === 'id') { + value = value[0]; + } + + return collection.table.getAll(value, {index: index.name}).limit(1); }); +} - if (aboveKeyCount !== 0) { - throw new Error('The keys in "above" must all appear in "order".'); - } else if (belowKeyCount !== 0) { - throw new Error('The keys in "below" must all appear in "order".'); - } +function getIndexValue(field, obj, bound, def) { + let value = getObjectField(obj, field); + if (value !== undefined) { return value; } + value = getObjectField(bound, field); + if (value !== undefined) { return value; } + return def; +} - // RSI: this is all wrong - const ordered_between = (obj) => Promise.resolve().then(() => { - const fuzzy_fields = object_to_fields(obj); - return collection.get_matching_index(fuzzy_fields, order_keys); - }).then((index) => { - order_keys.forEach((k) => { - if (obj[k] !== undefined) { - throw new Error(`"${k}" cannot be used in "order", "above", or "below" ` + - 'when finding by that field.'); - } - }); +function makeFindAllReql(collection, findAll, fixedFields, above, below, descending) { + return Promise.all(findAll.map((obj) => { + const fuzzyFields = objectToFields(obj); + // RSI: make sure fuzzyFields and fixedFields overlap only in the correct spot + // RSI: come up with some pathological tests that hit these sorts of cases - const get_bound = (option) => { - const eval_key = (key) => { - if (obj[key] !== undefined) { - return obj[key]; - } else if (option && option[0][key] !== undefined) { - return option[0][key]; - } else if (option && option[1] === 'open') { - return option === above ? r.maxval : r.minval; - } else { - return option === above ? r.minval : r.maxval; - } + return collection.get_matching_index(fuzzyFields, fixedFields).then((index) => { + const optargs = { + index: index.name, + leftBound: above ? above.bound : 'closed', + rightBound: below ? below.bound : 'closed', }; + let defaultLeftBound = r.minval; + let defaultRightBound = r.maxval; + + if (above && above.bound === 'open') { defaultLeftBound = r.maxval; } + if (below && below.bound === 'closed') { defaultRightBound = r.maxval; } + + let leftValue = index.fields.map((field) => + getIndexValue(field, obj, above && above.value, defaultLeftBound)); + let rightValue = index.fields.map((field) => + getIndexValue(field, obj, below && below.value, defaultRightBound)); + if (index.name === 'id') { - return eval_key('id'); + leftValue = leftValue[0]; + rightValue = rightValue[0]; } - return index.fields.map((k) => eval_key(k)); - }; - const above_value = get_bound(above); - const below_value = get_bound(below); + return collection.table + .orderBy({index: descending ? r.desc(index.name) : index.name}) + .between(leftValue || r.minval, rightValue || r.maxval, optargs); + }); + })).then((subqueries) => { + return r.union(...subqueries); + }); +} + +function makeTableScanReql(collection, fixedFields, above, below, descending) { + return collection.get_matching_index([], fixedFields).then((index) => { + let leftValue, rightValue; + const optargs = {index: index.name}; + + if (above) { + const defaultLeftBound = above.bound === 'closed' ? r.minval : r.maxval; + leftValue = index.fields.map((field) => getIndexValue(field, {}, above.value, defaultLeftBound)); + optargs.leftBound = above.bound; + } + if (below) { + const defaultRightBound = below.bound === 'closed' ? r.maxval : r.minval; + rightValue = index.fields.map((field) => getIndexValue(field, {}, below.value, defaultRightBound)); + optargs.rightBound = below.bound; + } - const optargs = { - index: index.name, - leftBound: above ? above[1] : 'closed', - rightBound: below ? below[1] : 'closed', - }; + if (index.name === 'id') { + if (leftValue) { leftValue = leftValue[0]; } + if (rightValue) { rightValue = rightValue[0]; } + } - return collection.table.orderBy({ - index: order && order[1] === 'descending' ? r.desc(index.name) : index.name, - }).between(above_value || r.minval, below_value || r.maxval, optargs); + let reql = collection.table.orderBy({index: descending ? r.desc(index.name) : index.name}); + if (leftValue || rightValue) { + reql = reql.between(leftValue || r.minval, rightValue || r.maxval, optargs); + } + return reql; }); +} - let reqlPromise; - if (find) { - reqlPromise = ordered_between(find).then((subquery) => subquery.limit(1)); - } else if (findAll && findAll.length > 1) { - reqlPromise = Promise.all( - findAll.map((x) => ordered_between(x))).then((subqueries) => - r.union.apply(subqueries)); - } else { - reqlPromise = ordered_between((findAll && findAll[0]) || {}); - } +function makeReadReql(req) { + return Promise.resolve().then(() => { + const collection = req.getParameter('collection'); + const findAll = req.getParameter('findAll'); + const find = req.getParameter('find'); + + assert(!find || !findAll); + + if (!collection) { + throw new Error('"collection" was not specified.'); + } + + if (find) { + return makeFindReql(collection, find); + } else { + const order = req.getParameter('order'); + const above = req.getParameter('above'); + const below = req.getParameter('below'); + const descending = Boolean(order && order.descending); + + const orderFields = order ? order.fields : []; + + if (above) { + if (order) { + if (!isSameField(above.field, orderFields[0])) { + throw new Error('"above" must be on the same field as the first in "order".'); + } + } else { + orderFields.push(above.field); + } + } - return reqlPromise.then((reql) => (limit !== undefined ? reql.limit(limit) : reql)); -}); + if (below) { + if (order || above) { + if (!isSameField(below.field, orderFields[0])) { + throw new Error('"below" must be on the same field as ' + + (order ? 'the first in "order"' : '"above"')); + } + } else { + orderFields.push(below.field); + } + } + + let reql_promise; + if (findAll) { + reql_promise = makeFindAllReql(collection, findAll, orderFields, above, below, descending); + } else { + reql_promise = makeTableScanReql(collection, orderFields, above, below, descending); + } + + const limit = req.getParameter('limit'); + return limit === undefined ? + reql_promise : reql_promise.then((reql) => reql.limit(limit)); + } + }); +} -module.exports = {makeReadReql}; +module.exports = {makeReadReql, objectToFields}; diff --git a/plugins/above/src/above.js b/plugins/above/src/above.js index db0d90611..8beb59ad4 100644 --- a/plugins/above/src/above.js +++ b/plugins/above/src/above.js @@ -1,18 +1,30 @@ 'use strict'; -const {isObject} = require('@horizon/plugin-utils'); +const {isObject, reads} = require('@horizon/plugin-utils'); +const {objectToFields} = reads; function above(req, res, next) { const args = req.options.above; if (args.length < 1 || args.length > 2) { next(new Error(`"above" expected 1 or 2 arguments but found ${args.length}.`)); - } else if (!isObject(args[0]) && typeof args[0] !== 'string') { - next(new Error('First argument to "above" must be a string or object.')); } else if (args.length === 2 && (args[1] !== 'open' && args[1] !== 'closed')) { next(new Error('Second argument to "above" must be "open" or "closed"')); } else { - req.setParameter({value: args[0], bound: args.length === 1 ? 'open' : args[1]}); - next(); + const bound = args.length === 1 ? 'open' : args[1]; + if (isObject(args[0])) { + const fields = objectToFields(args[0]); + if (fields.length !== 1) { + next(new Error('Object argument to "above" must have exactly one field.')); + } else { + req.setParameter({field: fields[0], value: args[0], bound}); + next(); + } + } else if (typeof args[0] === 'string') { + req.setParameter({field: ['id'], value: {id: args[0]}, bound}); + next(); + } else { + next(new Error('First argument to "above" must be a string or object.')); + } } } diff --git a/plugins/below/src/below.js b/plugins/below/src/below.js index 0eaee83b6..db5d1fffa 100644 --- a/plugins/below/src/below.js +++ b/plugins/below/src/below.js @@ -1,18 +1,30 @@ 'use strict'; -const {isObject} = require('@horizon/plugin-utils'); +const {isObject, reads} = require('@horizon/plugin-utils'); +const {objectToFields} = reads; function below(req, res, next) { const args = req.options.below; if (args.length < 1 || args.length > 2) { next(new Error(`"below" expected 1 or 2 arguments but found ${args.length}.`)); - } else if (!isObject(args[0]) && typeof args[0] !== 'string') { - next(new Error('First argument to "below" must be a string or object.')); } else if (args.length === 2 && (args[1] !== 'open' && args[1] !== 'closed')) { next(new Error('Second argument to "below" must be "open" or "closed"')); } else { - req.setParameter({value: args[0], bound: args.length === 1 ? 'closed' : args[1]}); - next(); + const bound = args.length === 1 ? 'closed' : args[1]; + if (isObject(args[0])) { + const fields = objectToFields(args[0]); + if (fields.length !== 1) { + next(new Error('Object argument to "below" must have exactly one field.')); + } else { + req.setParameter({field: fields[0], value: args[0], bound}); + next(); + } + } else if (typeof args[0] === 'string') { + req.setParameter({field: ['id'], value: {id: args[0]}, bound}); + next(); + } else { + next(new Error('First argument to "below" must be a string or object.')); + } } } diff --git a/plugins/collection/src/types/collection.js b/plugins/collection/src/types/collection.js index 4936ba01d..d45eb45af 100644 --- a/plugins/collection/src/types/collection.js +++ b/plugins/collection/src/types/collection.js @@ -93,12 +93,14 @@ class Collection { }; const match = this._get_table().get_matching_index(fuzzy_fields, ordered_fields); - if (match && !match.ready()) { - match.on_ready(done); - } else if (!match) { - this._create_index(fuzzy_fields.concat(ordered_fields), done); + if (match) { + if (match.ready()) { + resolve(match); + } else { + match.on_ready(done); + } } else { - resolve(match); + this._create_index(fuzzy_fields.concat(ordered_fields), done); } }); } diff --git a/plugins/fetch/src/fetch.js b/plugins/fetch/src/fetch.js index e51bfa4b9..797863024 100644 --- a/plugins/fetch/src/fetch.js +++ b/plugins/fetch/src/fetch.js @@ -13,9 +13,9 @@ function fetch(context) { } else if (!permissions) { next(new Error('"fetch" requires permissions to run')); } else { - reads.makeReadReql(req).then((reql) => - reql.run(conn, reqlOptions) - ).then((result) => { + reads.makeReadReql(req).then((reql) => { + return reql.run(conn, reqlOptions) + }).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { res.complete.then(() => { result.close().catch(() => { }); diff --git a/plugins/find/src/find.js b/plugins/find/src/find.js index 881cee7c5..46763bb94 100644 --- a/plugins/find/src/find.js +++ b/plugins/find/src/find.js @@ -8,6 +8,13 @@ function find(req, res, next) { next(new Error(`"find" expected 1 argument but found ${args.length}.`)); } else if (!isObject(args[0])) { next(new Error('First argument to "find" must be an object.')); + } else if (req.options.findAll || + req.options.limit || + req.options.order || + req.options.above || + req.options.below) { + next(new Error('"find" cannot be used with ' + + '"findAll", "limit", "order", "above", or "below"')); } else { req.setParameter(args[0]); next(); diff --git a/plugins/findAll/src/findAll.js b/plugins/findAll/src/findAll.js index cd19e30ac..259a59d4a 100644 --- a/plugins/findAll/src/findAll.js +++ b/plugins/findAll/src/findAll.js @@ -8,6 +8,8 @@ function findAll(req, res, next) { next(new Error(`"findAll" expected 1 or more arguments but found ${args.length}.`)); } else if (!args.every((val) => isObject(val))) { next(new Error('All arguments to "findAll" must be objects.')); + } else if (req.options.find) { + next(new Error('"findAll" cannot be used with "find"')); } else { req.setParameter(args); next(); diff --git a/plugins/order/src/order.js b/plugins/order/src/order.js index 3c1e60b93..aa3817f6e 100644 --- a/plugins/order/src/order.js +++ b/plugins/order/src/order.js @@ -23,8 +23,8 @@ function order(req, res, next) { next(new Error('Second argument to "order" must be "ascending" or "descending"')); } else { req.setParameter({ - fields: Array.isArray(args[0]) ? args[0].map(convertField) : convertField(args[0]), - direction: args.length === 1 ? 'ascending' : args[1], + fields: Array.isArray(args[0]) ? args[0].map(convertField) : [convertField(args[0])], + descending: args.length === 1 ? false : (args[1] === 'descending'), }); next(); } diff --git a/test/src/test/fetch.js b/test/src/test/fetch.js index 6067f9c83..00c7bd504 100644 --- a/test/src/test/fetch.js +++ b/test/src/test/fetch.js @@ -424,6 +424,24 @@ const all_tests = (collection) => { done(); }); }); + + it('findAll "above" and "below" on different fields.', (done) => { + utils.stream_test( + { + request_id: 0, + options: { + collection: [collection], + findAll: [{value: 0}], + below: [{a: 4}], + above: [{b: 5}], + fetch: [], + }, + }, + (err) => { + utils.check_error(err, '"below" must be on the same field as "above"'); + done(); + }); + }); }; const suite = (collection) => describe('Fetch', () => all_tests(collection)); diff --git a/test/src/test/permissions.js b/test/src/test/permissions.js index be716f602..1d688f863 100644 --- a/test/src/test/permissions.js +++ b/test/src/test/permissions.js @@ -56,30 +56,34 @@ const all_tests = (collection) => { before('Create user row', () => r.table('users').insert(context).run(utils.rdb_conn())); - const run = (options, validator) => new Promise((resolve, reject) => { - // Write group row into database + beforeEach('Authenticate', (done) => utils.horizon_default_auth(done)); + + const run = (rawOptions, validator) => new Promise((resolve, reject) => { + // Write group row into database r.table('hz_groups').insert( - {id: 'default', rules: r.literal({test: {template: 'any()', validator}})}, - {conflict: 'update'}).run(utils.rdb_conn()); - - // Construct query and send on websocket - utils.stream_test({request_id: 1, options}, (err, res) => { - if (err) { - reject(err); - } else { - resolve(res); - } - }); + {id: 'default', rules: {test: {template: 'any()', validator}}}, + {conflict: 'replace'}).run(utils.rdb_conn()).then(() => { + // TODO: this seems a bit racy - no guarantee that horizon will be up-to-date + // Construct request and send on websocket + const options = Object.assign({collection: [collection]}, rawOptions); + utils.stream_test({request_id: 1, options: options}, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + }).catch(reject); }); - describe('query', () => { + describe('fetch', () => { it('permitted', () => - run({order: [['id'], 'ascending'], query: []}, permitted_validator).then((res) => { + run({order: [['id'], 'ascending'], fetch: []}, permitted_validator).then((res) => { assert.deepStrictEqual(res, table_data); })); it('half-permitted', () => - run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], query: []}, + run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], fetch: []}, user_permitted_validator).then(() => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { @@ -91,7 +95,7 @@ const all_tests = (collection) => { })); it('forbidden', () => - run({query: []}, forbidden_validator).then(() => { + run({fetch: []}, forbidden_validator).then(() => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { assert.strictEqual(err.message, 'Operation not permitted.'); @@ -114,7 +118,7 @@ const all_tests = (collection) => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { assert.strictEqual(err.message, 'Operation not permitted.'); - // Check that we got the permitted row or nothing (race condition) + // Check that we got the permitted row or nothing (race condition) if (err.results.length !== 0) { assert.deepStrictEqual(err.results, [{id: 3}]); } From f61425c097a7c2b59de6ccf25bb5e97417d1fe48 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 11 Sep 2016 01:07:06 -0700 Subject: [PATCH 81/86] changed how user info is passed to validators --- plugins/fetch/src/fetch.js | 12 +++++++----- plugins/permissions/src/permissions.js | 4 +++- plugins/permissions/src/validator.js | 2 ++ plugins/remove/src/remove.js | 2 +- plugins/replace/src/replace.js | 2 +- plugins/store/src/store.js | 2 +- plugins/update/src/update.js | 3 +-- plugins/upsert/src/upsert.js | 3 +-- plugins/watch/src/watch.js | 4 ++-- test/src/test/fetch.js | 2 +- test/src/test/permissions.js | 19 ++++++++++++++++--- test/src/test/prereq.js | 2 +- test/src/test/protocol.js | 2 +- test/src/test/utils.js | 16 ++++++++-------- test/src/test/watch.js | 2 +- test/src/test/write.js | 8 ++++---- 16 files changed, 51 insertions(+), 34 deletions(-) diff --git a/plugins/fetch/src/fetch.js b/plugins/fetch/src/fetch.js index 797863024..596278e08 100644 --- a/plugins/fetch/src/fetch.js +++ b/plugins/fetch/src/fetch.js @@ -24,7 +24,7 @@ function fetch(context) { // TODO: reuse cursor batching return result.eachAsync((item) => { const validator = permissions(); - if (validator && !validator(req.clientCtx, item)) { + if (validator && !validator(item)) { next(new Error('Operation not permitted.')); result.close().catch(() => { }); } else { @@ -36,13 +36,15 @@ function fetch(context) { } else { const validator = permissions(); if (result !== null && result.constructor.name === 'Array') { - for (const item of result) { - if (validator && !validator(req.clientCtx, item)) { - return next(new Error('Operation not permitted.')); + if (validator) { + for (const item of result) { + if (!validator(item)) { + return next(new Error('Operation not permitted.')); + } } } res.end(result); - } else if (validator && !validator(req.clientCtx, result)) { + } else if (validator && !validator(result)) { next(new Error('Operation not permitted.')); } else { res.end([result]); diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 3fac49574..4394fe046 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -151,6 +151,7 @@ class UserCache { cfeed.unreadyAt = new Date(); }, onChange: (change) => { + cfeed.userRow = change.new_val || null; cfeed.unreadyAt = null; // We're ready on every change. const newGroups = new Set((change.new_val && change.new_val.groups) || []); oldGroups.forEach((g) => { @@ -171,6 +172,7 @@ class UserCache { cfeed.unreadyAt = new Date(0); // epoch } cfeed.refcount = 0; + cfeed.userRow = null; return cfeed; }; @@ -308,7 +310,7 @@ class UserCache { return (...args) => { try { for (const rule of ruleset) { - if (rule.isValid(...args)) { + if (rule.isValid(cfeed.userRow, ...args)) { return rule; } } diff --git a/plugins/permissions/src/validator.js b/plugins/permissions/src/validator.js index 159a57d99..3a9cf8db5 100644 --- a/plugins/permissions/src/validator.js +++ b/plugins/permissions/src/validator.js @@ -16,6 +16,8 @@ class Validator { } isValid(...args) { + console.log(`calling validator ${this._fn}`); + console.log(` with args ${JSON.stringify(args)}`); return this._fn(...args); } } diff --git a/plugins/remove/src/remove.js b/plugins/remove/src/remove.js index 33e6ed9cb..fc6ec0109 100644 --- a/plugins/remove/src/remove.js +++ b/plugins/remove/src/remove.js @@ -23,7 +23,7 @@ function remove(context) { .map((id) => collection.table.get(id)) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_required(validator, request.clientCtx, row, info, null), + writes.validate_old_row_required(validator, row, info, null), (rows) => // write to database, all valid rows r.expr(rows).do((row_data) => row_data.forEach((info) => diff --git a/plugins/replace/src/replace.js b/plugins/replace/src/replace.js index 5223f153f..ac784d501 100644 --- a/plugins/replace/src/replace.js +++ b/plugins/replace/src/replace.js @@ -23,7 +23,7 @@ function replace(context) { .map((id) => collection.table.get(id)) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_required(validator, request.clientCtx, row, info, row), + writes.validate_old_row_required(validator, row, info, row), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => diff --git a/plugins/store/src/store.js b/plugins/store/src/store.js index 520cf48e9..bf7414bd6 100644 --- a/plugins/store/src/store.js +++ b/plugins/store/src/store.js @@ -23,7 +23,7 @@ function store(context) { .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_optional(validator, request.clientCtx, row, info, row), + writes.validate_old_row_optional(validator, row, info, row), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => diff --git a/plugins/update/src/update.js b/plugins/update/src/update.js index b82e242e7..aee3d9a3f 100644 --- a/plugins/update/src/update.js +++ b/plugins/update/src/update.js @@ -27,8 +27,7 @@ function update(context) { [old_row, old_row.merge(new_row)]))) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_required( - validator, request.clientCtx, row, info[0], info[1]), + writes.validate_old_row_required(validator, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => diff --git a/plugins/upsert/src/upsert.js b/plugins/upsert/src/upsert.js index 82610c574..c811ed8e4 100644 --- a/plugins/upsert/src/upsert.js +++ b/plugins/upsert/src/upsert.js @@ -29,8 +29,7 @@ function upsert(context) { [null, new_row])) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_optional( - validator, request.clientCtx, row, info[0], info[1]), + writes.validate_old_row_optional(validator, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) .forEach((new_row) => diff --git a/plugins/watch/src/watch.js b/plugins/watch/src/watch.js index 9acef04bd..7b8baeb35 100644 --- a/plugins/watch/src/watch.js +++ b/plugins/watch/src/watch.js @@ -36,8 +36,8 @@ function watch(context) { } else { const validator = permissions(); if (validator) { - if ((item.old_val && !validator(req.clientCtx, item.old_val)) || - (item.new_val && !validator(req.clientCtx, item.new_val))) { + if ((item.old_val && !validator(item.old_val)) || + (item.new_val && !validator(item.new_val))) { next(new Error('Operation not permitted.')); } } diff --git a/test/src/test/fetch.js b/test/src/test/fetch.js index 00c7bd504..121a10f34 100644 --- a/test/src/test/fetch.js +++ b/test/src/test/fetch.js @@ -10,7 +10,7 @@ const all_tests = (collection) => { before('Clear collection', () => utils.clear_collection(collection)); before('Populate collection', () => utils.populate_collection(collection, num_rows)); - beforeEach('Authenticate client', utils.horizon_admin_auth); + beforeEach('Authenticate client', (done) => utils.horizon_token_auth('admin', done)); it('collection scan.', (done) => { utils.stream_test( diff --git a/test/src/test/permissions.js b/test/src/test/permissions.js index 1d688f863..65294bcd3 100644 --- a/test/src/test/permissions.js +++ b/test/src/test/permissions.js @@ -7,7 +7,7 @@ const assert = require('assert'); const r = require('rethinkdb'); const user_id = 3; -const context = { user: { id: user_id } }; +const user_row = { id: user_id, groups: ['default'] }; // Permit all rows const permitted_validator = ` @@ -54,9 +54,9 @@ const all_tests = (collection) => { beforeEach('Populate test table', () => r.table(collection).insert(table_data).run(utils.rdb_conn())); before('Create user row', () => - r.table('users').insert(context).run(utils.rdb_conn())); + r.table('users').insert(user_row).run(utils.rdb_conn())); - beforeEach('Authenticate', (done) => utils.horizon_default_auth(done)); + beforeEach('Authenticate', (done) => utils.horizon_unauthenticated_auth(done)); const run = (rawOptions, validator) => new Promise((resolve, reject) => { // Write group row into database @@ -137,6 +137,7 @@ const all_tests = (collection) => { it('permitted', () => run({insert: [{id: 11}]}, permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 11); return r.table(collection).get(11).eq(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -145,6 +146,7 @@ const all_tests = (collection) => { it('permitted based on context', () => run({insert: [{id: 13}]}, user_permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 13); return r.table(collection).get(13).eq(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -169,6 +171,7 @@ const all_tests = (collection) => { it('permitted', () => run({store: [{id: 11}]}, permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 11); return r.table(collection).get(11).eq(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -177,6 +180,7 @@ const all_tests = (collection) => { it('permitted based on context', () => run({store: [{id: 13}]}, user_permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 13); return r.table(collection).get(13).eq(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -201,6 +205,7 @@ const all_tests = (collection) => { it('permitted', () => run({upsert: [{id: 11}]}, permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 11); return r.table(collection).get(11).eq(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -209,6 +214,7 @@ const all_tests = (collection) => { it('permitted based on context', () => run({upsert: [{id: 13}]}, user_permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 13); return r.table(collection).get(13).eq(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -233,6 +239,7 @@ const all_tests = (collection) => { it('permitted', () => run({update: [{id: 1, value: 5}]}, permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 1); return r.table(collection).get(1).hasFields('value').not() .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -241,6 +248,7 @@ const all_tests = (collection) => { it('permitted based on context', () => run({update: [{id: 3, value: 5}]}, user_permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 3); return r.table(collection).get(3).hasFields('value').not() .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -265,6 +273,7 @@ const all_tests = (collection) => { it('permitted', () => run({replace: [{id: 1, value: 5}]}, permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 1); return r.table(collection).get(1).hasFields('value').not() .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -273,6 +282,7 @@ const all_tests = (collection) => { it('permitted based on context', () => run({replace: [{id: 3, value: 5}]}, user_permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 3); return r.table(collection).get(3).hasFields('value').not() .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -297,6 +307,7 @@ const all_tests = (collection) => { it('permitted', () => run({remove: [{id: 1}]}, permitted_validator).then((res) => { assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 1); return r.table(collection).get(1).ne(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); @@ -304,7 +315,9 @@ const all_tests = (collection) => { it('permitted based on context', () => run({remove: [{id: 3}]}, user_permitted_validator).then((res) => { + console.log(`write result: ${JSON.stringify(res)}`); assert.strictEqual(res.length, 1); + assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 3); return r.table(collection).get(3).ne(null) .branch(r.error('write did not go through'), null).run(utils.rdb_conn()); diff --git a/test/src/test/prereq.js b/test/src/test/prereq.js index 06d3c649c..099e09df0 100644 --- a/test/src/test/prereq.js +++ b/test/src/test/prereq.js @@ -7,7 +7,7 @@ const crypto = require('crypto'); const all_tests = (collection) => { beforeEach('clear collection', () => utils.clear_collection(collection)); - beforeEach('authenticate', (done) => utils.horizon_admin_auth(done)); + beforeEach('authenticate', (done) => utils.horizon_token_auth('admin', done)); // Launch simultaneous queries that depend on a non-existent collection, then // verify that only one table exists for that collection. diff --git a/test/src/test/protocol.js b/test/src/test/protocol.js index 719fb74e6..a5d8d2ebf 100644 --- a/test/src/test/protocol.js +++ b/test/src/test/protocol.js @@ -5,7 +5,7 @@ const utils = require('./utils'); const assert = require('assert'); const all_tests = (collection) => { - beforeEach('Authenticate client', utils.horizon_admin_auth); + beforeEach('Authenticate client', (done) => utils.horizon_token_auth('admin', done)); it('unparseable', (done) => { const conn = utils.horizon_conn(); diff --git a/test/src/test/utils.js b/test/src/test/utils.js index 7a6b69940..9884af167 100644 --- a/test/src/test/utils.js +++ b/test/src/test/utils.js @@ -116,10 +116,10 @@ const table = (collection) => r.error('Collection does not exist.'), row('id')))); -const make_admin_token = () => { +const make_token = (id) => { const jwt = horizon_server && horizon_server._auth && horizon_server._auth._jwt; assert(jwt); - return jwt.sign({id: 'admin', provider: null}).token; + return jwt.sign({id, provider: null}).token; }; // Creates a collection, no-op if it already exists, uses horizon server prereqs @@ -132,7 +132,7 @@ function create_collection(collection) { {rejectUnauthorized: false}) .once('error', (err) => assert.ifError(err)) .on('open', () => { - conn.send(JSON.stringify({request_id: 123, method: 'token', token: make_admin_token()})); + conn.send(JSON.stringify({request_id: 123, method: 'token', token: make_token('admin')})); conn.once('message', (data) => { const res = JSON.parse(data); assert.strictEqual(res.request_id, 123); @@ -241,17 +241,17 @@ const horizon_auth = (req, cb) => { }; // Create a token for the admin user and use that to authenticate -const horizon_admin_auth = (done) => { - horizon_auth({request_id: -1, method: 'token', token: make_admin_token()}, (res) => { +const horizon_token_auth = (id, done) => { + horizon_auth({request_id: -1, method: 'token', token: make_token(id)}, (res) => { assert.strictEqual(res.request_id, -1); assert.strictEqual(typeof res.token, 'string'); - assert.strictEqual(res.id, 'admin'); + assert.strictEqual(res.id, id); assert.strictEqual(res.provider, null); done(); }); }; -const horizon_default_auth = (done) => { +const horizon_unauthenticated_auth = (done) => { horizon_auth({request_id: -1, method: 'unauthenticated'}, (res) => { assert.strictEqual(res.request_id, -1); assert.strictEqual(typeof res.token, 'string'); @@ -320,7 +320,7 @@ module.exports = { clear_collection, open_horizon_conn, close_horizon_conn, - horizon_auth, horizon_admin_auth, horizon_default_auth, + horizon_auth, horizon_token_auth, horizon_unauthenticated_auth, add_horizon_listener, remove_horizon_listener, set_group, diff --git a/test/src/test/watch.js b/test/src/test/watch.js index eb9d2429b..1f59b8b30 100644 --- a/test/src/test/watch.js +++ b/test/src/test/watch.js @@ -7,7 +7,7 @@ const all_tests = (collection) => { before('Clear collection', () => utils.clear_collection(collection)); before('Populate collection', () => utils.populate_collection(collection, num_rows)); - beforeEach('Authenticate client', utils.horizon_admin_auth); + beforeEach('Authenticate client', (done) => utils.horizon_token_auth('admin', done)); }; const suite = (collection) => describe('Subscribe', () => all_tests(collection)); diff --git a/test/src/test/write.js b/test/src/test/write.js index 4d36344ce..1f865075a 100644 --- a/test/src/test/write.js +++ b/test/src/test/write.js @@ -83,7 +83,7 @@ const all_tests = (collection) => { beforeEach('Clear collection', () => utils.clear_collection(collection)); describe('Basic writes', () => { - beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); + beforeEach('Authenticate', (done) => utils.horizon_token_auth('admin', done)); beforeEach('Populate collection', () => utils.populate_collection(collection, original_data)); const request_from_ids = (type, ids) => make_request(type, ids.map(new_row_from_id)); @@ -209,7 +209,7 @@ const all_tests = (collection) => { }); describe('Versioned', () => { - beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); + beforeEach('Authenticate', (done) => utils.orizon_token_auth('admin', done)); const test_data = [{id: 'versioned', [hz_v]: 11, foo: 'bar'}]; beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); @@ -326,7 +326,7 @@ const all_tests = (collection) => { }); describe('Versionless', () => { - beforeEach('Authenticate', (done) => utils.horizon_admin_auth(done)); + beforeEach('Authenticate', (done) => utils.horizon_token_auth('admin', done)); const test_data = [{id: 'versionless', foo: 'bar'}]; beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); @@ -447,7 +447,7 @@ const all_tests = (collection) => { // write will make it through each loop, although it is undefined in which order // the writes occur. describe('Retry', () => { - beforeEach('Authenticate', (done) => utils.horizon_default_auth(done)); + beforeEach('Authenticate', (done) => utils.horizon_unauthenticated_auth(done)); // Set a catch-all rule for the 'default' group so we can have a validator before('Set rules', (done) => utils.set_group({ From bddb2403b2a56c8448b484cd89e6d961f9b1b222 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 11 Sep 2016 05:28:10 -0700 Subject: [PATCH 82/86] fixed all server tests --- plugin-utils/src/writes.js | 8 ++--- plugins/insert/src/insert.js | 2 +- plugins/permissions/src/permissions.js | 1 + plugins/permissions/src/validator.js | 2 -- plugins/watch/src/watch.js | 1 + server/src/response.js | 2 +- test/src/test/permissions.js | 44 ++++++++++---------------- test/src/test/protocol.js | 4 +-- test/src/test/write.js | 2 +- 9 files changed, 28 insertions(+), 38 deletions(-) diff --git a/plugin-utils/src/writes.js b/plugin-utils/src/writes.js index 1ed9791f2..a76c08ffe 100644 --- a/plugin-utils/src/writes.js +++ b/plugin-utils/src/writes.js @@ -142,12 +142,12 @@ function retry_loop(original_rows, .then(make_write_response); } -function validate_old_row_optional(validator, context, original, old_row, new_row) { +function validate_old_row_optional(validator, original, old_row, new_row) { const expected_version = original[hz_v]; if (expected_version !== undefined && (!old_row || expected_version !== old_row[hz_v])) { return new Error(invalidated_msg); - } else if (!validator(context, old_row, new_row)) { + } else if (!validator(old_row, new_row)) { return new Error(unauthorized_msg); } @@ -159,7 +159,7 @@ function validate_old_row_optional(validator, context, original, old_row, new_ro } } -function validate_old_row_required(validator, context, original, old_row, new_row) { +function validate_old_row_required(validator, original, old_row, new_row) { if (old_row == null) { return new Error(missing_msg); } @@ -169,7 +169,7 @@ function validate_old_row_required(validator, context, original, old_row, new_ro if (expected_version !== undefined && expected_version !== old_version) { return new Error(invalidated_msg); - } else if (!validator(context, old_row, new_row)) { + } else if (!validator(old_row, new_row)) { return new Error(unauthorized_msg); } diff --git a/plugins/insert/src/insert.js b/plugins/insert/src/insert.js index 2da6befbc..e4d285246 100644 --- a/plugins/insert/src/insert.js +++ b/plugins/insert/src/insert.js @@ -20,7 +20,7 @@ function insert(context) { (rows) => // pre-validation, all rows Array(rows.length).fill(null), (validator, row, info) => { // validation, each row - if (!validator(request.clientCtx, info, row)) { + if (!validator(info, row)) { return new Error(writes.unauthorized_msg); } }, diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index 4394fe046..ce7f6de04 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -181,6 +181,7 @@ class UserCache { this.userCfeeds.set(null, Object.assign(new Reliable(), { refcount: 1, unreadyAt: null, + userRow: {id: null, groups: ['default']}, readyPromise: Promise.resolve() })); diff --git a/plugins/permissions/src/validator.js b/plugins/permissions/src/validator.js index 3a9cf8db5..159a57d99 100644 --- a/plugins/permissions/src/validator.js +++ b/plugins/permissions/src/validator.js @@ -16,8 +16,6 @@ class Validator { } isValid(...args) { - console.log(`calling validator ${this._fn}`); - console.log(` with args ${JSON.stringify(args)}`); return this._fn(...args); } } diff --git a/plugins/watch/src/watch.js b/plugins/watch/src/watch.js index 7b8baeb35..02f55d6df 100644 --- a/plugins/watch/src/watch.js +++ b/plugins/watch/src/watch.js @@ -39,6 +39,7 @@ function watch(context) { if ((item.old_val && !validator(item.old_val)) || (item.new_val && !validator(item.new_val))) { next(new Error('Operation not permitted.')); + return; } } res.write([item]); diff --git a/server/src/response.js b/server/src/response.js index d74addb92..7bdca5a64 100644 --- a/server/src/response.js +++ b/server/src/response.js @@ -15,7 +15,7 @@ class Response { this._completed = true; logger.debug(`Request failed with error: ${err.stack}`); this._socketSend({ - error: `${err}`, + error: `${err.message}`, error_code: err.code || -1, }); throw err; diff --git a/test/src/test/permissions.js b/test/src/test/permissions.js index 65294bcd3..1ba5456fb 100644 --- a/test/src/test/permissions.js +++ b/test/src/test/permissions.js @@ -6,44 +6,36 @@ const assert = require('assert'); const r = require('rethinkdb'); -const user_id = 3; -const user_row = { id: user_id, groups: ['default'] }; +const userId = 3; +const userRow = { id: userId, groups: ['default'] }; // Permit all rows const permitted_validator = ` -(context) => { - if (!context) { throw new Error('no context'); } +(userRow) => { + if (!userRow) { throw new Error('Validator was not passed a user row.'); } return true; } `; // Forbid all rows const forbidden_validator = ` -(context) => { - if (!context) { throw new Error('no context'); } +(userRow) => { + if (!userRow) { throw new Error('Validator was not passed a user row.'); } return false; } `; // Permit a row when the user's id is the last digit of the row's id const user_permitted_validator = ` -(context, a, b) => { - if (!context) { throw new Error('no context'); } - const value = (a && a.user.id) || (b && b.user.id); - return context.id === (value % 10); +(userRow, a, b) => { + if (!userRow) { throw new Error('Validator was not passed a user row.'); } + const value = (a && a.id) || (b && b.id); + return userRow.id === (value % 10); } `; const all_tests = (collection) => { describe('Validation', () => { - const metadata = { - collection: () => ({ - table: r.table(collection), - get_matching_index: () => ({name: 'id', fields: ['id']}), - }), - connection: () => utils.rdb_conn(), - }; - const table_data = []; for (let i = 0; i < 10; ++i) { table_data.push({id: i}); @@ -54,9 +46,9 @@ const all_tests = (collection) => { beforeEach('Populate test table', () => r.table(collection).insert(table_data).run(utils.rdb_conn())); before('Create user row', () => - r.table('users').insert(user_row).run(utils.rdb_conn())); + r.table('users').insert(userRow).run(utils.rdb_conn())); - beforeEach('Authenticate', (done) => utils.horizon_unauthenticated_auth(done)); + beforeEach('Authenticate', (done) => utils.horizon_token_auth(userId, done)); const run = (rawOptions, validator) => new Promise((resolve, reject) => { // Write group row into database @@ -68,6 +60,7 @@ const all_tests = (collection) => { const options = Object.assign({collection: [collection]}, rawOptions); utils.stream_test({request_id: 1, options: options}, (err, res) => { if (err) { + err.results = res; reject(err); } else { resolve(res); @@ -88,7 +81,7 @@ const all_tests = (collection) => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { assert.strictEqual(err.message, 'Operation not permitted.'); - // Check that we got the permitted row or nothing (race condition) + // Check that we got the permitted row or nothing (race condition) if (err.results.length !== 0) { assert.deepStrictEqual(err.results, [{id: 3}]); } @@ -113,15 +106,13 @@ const all_tests = (collection) => { }); it('half-permitted', () => - run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], watch: []}, + run({order: [['id'], 'ascending'], above: [{id: 3}, 'closed'], limit: [3], watch: []}, user_permitted_validator).then(() => { assert(false, 'Read should not have been permitted.'); }).catch((err) => { assert.strictEqual(err.message, 'Operation not permitted.'); - // Check that we got the permitted row or nothing (race condition) - if (err.results.length !== 0) { - assert.deepStrictEqual(err.results, [{id: 3}]); - } + assert.strictEqual(err.results.length, 1); + assert.deepStrictEqual(err.results[0].new_val.id, 3); })); it('forbidden', () => @@ -315,7 +306,6 @@ const all_tests = (collection) => { it('permitted based on context', () => run({remove: [{id: 3}]}, user_permitted_validator).then((res) => { - console.log(`write result: ${JSON.stringify(res)}`); assert.strictEqual(res.length, 1); assert.strictEqual(res[0].error, undefined); assert.strictEqual(res[0].id, 3); diff --git a/test/src/test/protocol.js b/test/src/test/protocol.js index a5d8d2ebf..31fcb10d3 100644 --- a/test/src/test/protocol.js +++ b/test/src/test/protocol.js @@ -61,7 +61,7 @@ const all_tests = (collection) => { utils.stream_test({request_id: 2, options: {above: []}}, (err, res) => { assert.deepStrictEqual(res, []); assert.strictEqual(err.message, - 'Error: No terminal method was specified in the request.'); + 'No terminal method was specified in the request.'); done(); }); }); @@ -70,7 +70,7 @@ const all_tests = (collection) => { utils.stream_test({request_id: 2, options: {fake: []}}, (err, res) => { assert.deepStrictEqual(res, []); assert.strictEqual(err.message, - 'Error: No method to handle option "fake".'); + 'No method to handle option "fake".'); done(); }); }); diff --git a/test/src/test/write.js b/test/src/test/write.js index 1f865075a..fe51005e5 100644 --- a/test/src/test/write.js +++ b/test/src/test/write.js @@ -209,7 +209,7 @@ const all_tests = (collection) => { }); describe('Versioned', () => { - beforeEach('Authenticate', (done) => utils.orizon_token_auth('admin', done)); + beforeEach('Authenticate', (done) => utils.horizon_token_auth('admin', done)); const test_data = [{id: 'versioned', [hz_v]: 11, foo: 'bar'}]; beforeEach('Populate collection', () => utils.populate_collection(collection, test_data)); From dc9ef135af091b144a54e4063b840fcf63c809cc Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Sun, 11 Sep 2016 06:06:26 -0700 Subject: [PATCH 83/86] got test server running --- test/package.json | 3 ++- test/src/serve.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/package.json b/test/package.json index 9f000687a..0cdffa4f7 100644 --- a/test/package.json +++ b/test/package.json @@ -6,7 +6,8 @@ "scripts": { "lint": "eslint src test", "test": "mocha --timeout 10000 dist/test", - "build": "babel src -d dist -s true" + "build": "babel src -d dist -s true", + "serve": "node ./dist/serve.js" }, "repository": { "type": "git", diff --git a/test/src/serve.js b/test/src/serve.js index 3bee55b55..3df9d9676 100755 --- a/test/src/serve.js +++ b/test/src/serve.js @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; -require('../server/node_modules/source-map-support').install(); +require('source-map-support').install(); Error.stackTraceLimit = Infinity; @@ -28,8 +28,8 @@ const crypto = require('crypto'); const argparse = require('argparse'); const data_dir = path.resolve(__dirname, 'rethinkdb_data_test'); -const test_dist_dir = path.resolve(__dirname, '../client/dist'); -const examples_dir = path.resolve(__dirname, '../examples'); +const test_dist_dir = path.resolve(__dirname, '../../client/dist'); +const examples_dir = path.resolve(__dirname, '../../examples'); const parser = new argparse.ArgumentParser(); parser.addArgument(['--port', '-p'], From 328d124a36d654b941027cc1651171d55dd5b98f Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 13 Sep 2016 00:34:12 -0700 Subject: [PATCH 84/86] checkpoint --- babelify.sh | 2 +- client/src/socket.js | 2 +- plugin-router/base/src/index.js | 30 +++++---- plugin-router/express/src/index.js | 98 +++++++++++++++++++++++++----- server/src/client.js | 3 - server/src/index.js | 36 +++++++++++ server/src/server.js | 23 ------- 7 files changed, 139 insertions(+), 55 deletions(-) diff --git a/babelify.sh b/babelify.sh index fca3b41cc..16dfe80cf 100755 --- a/babelify.sh +++ b/babelify.sh @@ -3,7 +3,7 @@ dirs="server plugins plugin-router plugin-utils test" for path in `find $dirs -name .babelrc | grep -v node_modules`; do { - babel ${path%%.babelrc}/src -d ${path%%.babelrc}/dist -s true -w + babel ${path%%.babelrc}src -d ${path%%.babelrc}dist -s true -w } & done diff --git a/client/src/socket.js b/client/src/socket.js index 0e501cfdd..9389aa38c 100644 --- a/client/src/socket.js +++ b/client/src/socket.js @@ -14,7 +14,7 @@ import 'rxjs/add/operator/publish' import { serialize, deserialize } from './serialization.js' -const PROTOCOL_VERSION = 'rethinkdb-horizon-v0' +const PROTOCOL_VERSION = 'rethinkdb-horizon-v1' // Before connecting the first time const STATUS_UNCONNECTED = { type: 'unconnected' } diff --git a/plugin-router/base/src/index.js b/plugin-router/base/src/index.js index 1b9fd6b20..1ed89f374 100644 --- a/plugin-router/base/src/index.js +++ b/plugin-router/base/src/index.js @@ -24,6 +24,7 @@ class PluginRouter extends EventEmitter { add(plugin, options) { if (!options.name) { + // RSI: make sure plugin names don't contain a '/' options.name = plugin.name; } @@ -52,32 +53,39 @@ class PluginRouter extends EventEmitter { addedMethods.forEach((m) => this.horizon.removeMethod(m)); throw err; } + + if (plugin.activate.length < 3) { + this._noteReady(options.name); + } + + // RSI: we probably need to return a few more things in 'active' (for use by + // sub-plugin-router implementations) + active.name = options.name; + active.plugin = plugin; + active.options = options; + return active; }); - if (plugin.activate.length < 3) { - activatePromise.then(() => this._noteReady(options.name)); - } - - this.plugins.set(options.name, {options, activatePromise, plugin}); + this.plugins.set(options.name, activatePromise); return activatePromise; } remove(name, reason) { - const info = this.plugins.get(name); + const activatePromise = this.plugins.get(name); - if (!info) { + if (!activatePromise) { return Promise.reject(new Error(`Plugin "${name}" is not present.`)); } this.plugins.delete(name); - return info.activatePromise.then((active) => { + return activatePromise.then((active) => { for (const m in active.methods) { this.horizon.removeMethod(m); } - if (info.plugin.deactivate) { - return info.plugin.deactivate(this.context, info.options, - reason || 'Removed from PluginRouter.'); + if (active.plugin.deactivate) { + return active.plugin.deactivate(this.context, active.options, + reason || 'Removed from PluginRouter.'); } }); } diff --git a/plugin-router/express/src/index.js b/plugin-router/express/src/index.js index 668b67591..c6b370a1e 100644 --- a/plugin-router/express/src/index.js +++ b/plugin-router/express/src/index.js @@ -1,27 +1,93 @@ 'use strict'; +const HorizonServer = require('@horizon/server'); const PluginRouterBase = require('@horizon/plugin-router-base'); -class PluginRouterExpress extends PluginRouterBase { - constructor(express, horizon) { - super(horizon); - this.express = express; - } +module.exports = (hz) => { + // Using local variables so we can later be removed without leaking things + let pluginRouter = new PluginRouterBase(horizonServer); + let expressRouter = express.Router(); + let routes = new Map(); // For recreating the router when a route is removed + let horizonServer = hz; + + const middleware = function (req, res, next) { + if (expressRouter) { + expressRouter(req, res, next); + } else { + next(); + } + }; - add(...args) { - return super.add(...args).then((res) => { - // RSI: add routes to express - return res; + const addHandler = function (path, handler) { + routes.set(path, active.http); + expressRouter.use(path, active.http); + }; + + middleware.add = function (...args) { + return Promise.resolve().then(() => { + if (!pluginRouter) { throw new Error('PluginRouter has been closed.'); } + return pluginRouter.add(...args); + }).then((active) => { + if (active.http) { + addHandler(`/${active.name}/`, active.http); + } + return active; }); } - remove(...args) { - return super.remove(...args).then((res) => { - // RSI: remove routes from express - return res; + middleware.remove = function (name, ...args) { + return Promise.resolve().then(() => { + if (!pluginRouter) { throw new Error('PluginRouter has been closed.'); } + + // Remove the route before deactivating the plugin + const path = `/${name}/`; + if (PluginRouterBase.isValidName(name) && routes.has(path)) { + routes.delete(`/${name}/`); + expressRouter = express.Router(); + routes.forEach((handler, path) => { + expressRouter.use(path, handler); + }); + } + + return pluginRouter.remove(...args); }); - } -}; + }; -module.exports = PluginRouterExpress; + let closePromise; + middleware.close = function (...args) { + if (!closePromise) { + expressRouter = null; + horizonServer = null; + closePromise = pluginRouter.close(...args); + pluginRouter = null; + } + return closePromise; + }; + + addHandler('/horizon.js', (req, res, next) => { + res.head('Content-Type', 'application/javascript'); + res.send(HorizonServer.clientSource()); + res.end(); + }); + + addHandler('/horizon.js.map', (req, res, next) => { + res.head('Content-Type', 'application/javascript'); + res.send(HorizonServer.clientSourceMap()); + res.end(); + }); + + addHandler('/horizon-core.js.map', (req, res, next) => { + res.head('Content-Type', 'application/javascript'); + res.send(HorizonServer.clientSourceCore()); + res.end(); + }); + + addHandler('/horizon-core.js.map', (req, res, next) => { + res.head('Content-Type', 'application/javascript'); + res.send(HorizonServer.clientSourceCoreMap()); + res.end(); + }); + + return middleware; +}; diff --git a/server/src/client.js b/server/src/client.js index 36642ace4..a6953964d 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -11,13 +11,11 @@ class ClientConnection { constructor(socket, auth, requestHandlerCb, - capabilitiesCb, clientEvents) { logger.debug('ClientConnection established.'); this.socket = socket; this.auth = auth; this.requestHandlerCb = requestHandlerCb; - this.capabilitiesCb = capabilitiesCb; this.clientEvents = clientEvents; this._context = { }; @@ -104,7 +102,6 @@ class ClientConnection { token: res.token, id: res.payload.id, provider: res.payload.provider, - capabilities: this.capabilitiesCb(), }); this.socket.on('message', (msg) => this.error_wrap_socket(() => this.handle_request(msg))); diff --git a/server/src/index.js b/server/src/index.js index fe130a1d9..5a493e4d3 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -25,3 +25,39 @@ module.exports.Reliable = reliable.Reliable; module.exports.ReliableConn = reliable.ReliableConn; module.exports.ReliableUnion = reliable.ReliableUnion; module.exports.ReliableChangefeed = reliable.ReliableChangefeed; + +const fs = require('fs'); +const clientSourcePath = require.resolve('@horizon/client/dist/horizon'); +const clientSourceCorePath = require.resolve('@horizon/client/dist/horizon-core'); + +let clientSource, clientSourceMap, clientSourceCore, clientSourceCoreMap; + +// Load these lazily (but synchronously) +module.exports.clientSource = function() { + if (!clientSource) { + clientSource = fs.readFileSync(clientSourcePath); + } + return clientSource; +} + +module.exports.clientSourceMap = function() { + if (!clientSourceMap) { + clientSourceMap = fs.readFileSync(`${clientSourcePath}.map`); + } + return clientSourceMap; +} + +modules.exports.clientSourceCore = function() { + if (!clientSourceCore) { + clientSourceCore = fs.readFileSync(clientSourceCorePath); + } + return clientSourceCore; +} + +modules.exports.clientSourceCoreMap = function() { + if (!clientSourceCoreMap) { + clientSourceCoreMap = fs.readFileSync(`${clientSourceCorePath}.map`); + } + return clientSourceCoreMap; +} + diff --git a/server/src/server.js b/server/src/server.js index 1ad5cd9fe..6be353ce4 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -140,7 +140,6 @@ class Server { socket, this._auth, this._requestHandler, - this._getCapabilities, this.events, ); this._clients.add(client); @@ -183,14 +182,12 @@ class Server { } this._requirementsOrdering = null; - this._capabilities = null; } removeMethod(name) { delete this._methods[name]; this._middlewareMethods.delete(name); this._requirementsOrdering = null; - this._capabilities = null; } _getRequirementsOrdering() { @@ -214,26 +211,6 @@ class Server { return this._requirementsOrdering; } - _getCapabilities() { - if (!this._capabilities) { - this._capabilities = {options: [], terminals: []}; - for (const k in this._methods) { - const method = this._methods[k]; - switch (method.type) { - case 'option': - this._capabilities.options.push(k); - break; - case 'terminal': - this._capabilities.terminals.push(k); - break; - default: - break; - } - } - } - return this._capabilities; - } - // TODO: We close clients in `onUnready` above, but don't wait for them to be closed. close() { if (!this._close_promise) { From da538ed138ae7552a402eab3c32e8067370dfc19 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Tue, 13 Sep 2016 04:05:08 -0700 Subject: [PATCH 85/86] camelCasing and fixing tests --- client/src/ast.js | 52 +++-- plugin-router/{base => }/.eslintrc.js | 3 +- plugin-router/base/src/index.js | 2 +- plugin-router/express/.eslintrc.js | 16 -- plugin-router/express/src/index.js | 53 +++-- plugin-router/express/src/request.js | 25 -- plugin-router/hapi/.eslintrc.js | 16 -- plugin-router/hapi/src/index.js | 14 +- plugin-router/hapi/src/request.js | 25 -- plugin-router/http/.eslintrc.js | 16 -- plugin-router/http/src/index.js | 14 +- plugin-router/http/src/request.js | 25 -- plugin-router/koa/.eslintrc.js | 16 -- plugin-router/koa/src/index.js | 14 +- plugin-router/koa/src/request.js | 25 -- .../src/auth-utils.js | 0 plugin-utils/src/reads.js | 6 +- plugins/.eslintrc.js | 1 + .../src/auth => plugins/auth0/src}/auth0.js | 0 plugins/collection/src/collection.js | 12 +- plugins/collection/src/indexes.js | 16 +- plugins/collection/src/types/collection.js | 36 +-- plugins/collection/src/types/index.js | 30 +-- plugins/collection/src/types/metadata.js | 191 +++++++-------- plugins/collection/src/types/table.js | 46 ++-- .../auth => plugins/facebook/src}/facebook.js | 0 .../src/auth => plugins/github/src}/github.js | 0 .../src/auth => plugins/google/src}/google.js | 0 plugins/insert/src/insert.js | 6 +- plugins/permissions/src/group.js | 8 +- plugins/permissions/src/permissions.js | 17 +- plugins/permissions/src/rule.js | 2 +- plugins/permissions/src/template.js | 53 +++-- plugins/permissions/src/test/template.js | 219 +++++++++--------- plugins/permissions/src/test/validator.js | 43 ++-- plugins/permissions/src/validator.js | 6 +- plugins/remove/src/remove.js | 20 +- plugins/replace/src/replace.js | 24 +- .../src/auth => plugins/slack/src}/slack.js | 0 plugins/store/src/store.js | 32 +-- .../src/auth => plugins/twitch/src}/twitch.js | 0 .../auth => plugins/twitter/src}/twitter.js | 0 plugins/update/src/update.js | 32 +-- plugins/upsert/src/upsert.js | 46 ++-- plugins/watch/src/watch.js | 8 +- server/src/auth.js | 39 ++-- server/src/client.js | 70 +++--- server/src/error.js | 40 ---- server/src/index.js | 12 +- ...actory_factory_pattern_strategy_factory.js | 0 server/src/reliable.js | 24 +- server/src/request.js | 4 +- .../{schema/server_options.js => schema.js} | 24 +- server/src/schema/horizon_protocol.js | 23 -- server/src/server.js | 50 ++-- 55 files changed, 642 insertions(+), 814 deletions(-) rename plugin-router/{base => }/.eslintrc.js (78%) delete mode 100644 plugin-router/express/.eslintrc.js delete mode 100644 plugin-router/express/src/request.js delete mode 100644 plugin-router/hapi/.eslintrc.js delete mode 100644 plugin-router/hapi/src/request.js delete mode 100644 plugin-router/http/.eslintrc.js delete mode 100644 plugin-router/http/src/request.js delete mode 100644 plugin-router/koa/.eslintrc.js delete mode 100644 plugin-router/koa/src/request.js rename server/src/auth/utils.js => plugin-utils/src/auth-utils.js (100%) rename {server/src/auth => plugins/auth0/src}/auth0.js (100%) rename {server/src/auth => plugins/facebook/src}/facebook.js (100%) rename {server/src/auth => plugins/github/src}/github.js (100%) rename {server/src/auth => plugins/google/src}/google.js (100%) rename {server/src/auth => plugins/slack/src}/slack.js (100%) rename {server/src/auth => plugins/twitch/src}/twitch.js (100%) rename {server/src/auth => plugins/twitter/src}/twitter.js (100%) delete mode 100644 server/src/error.js delete mode 100644 server/src/middleware_pattern_factory_factory_pattern_strategy_factory.js rename server/src/{schema/server_options.js => schema.js} (55%) delete mode 100644 server/src/schema/horizon_protocol.js diff --git a/client/src/ast.js b/client/src/ast.js index e6420a163..61b84516f 100644 --- a/client/src/ast.js +++ b/client/src/ast.js @@ -41,10 +41,15 @@ function checkIfLegalToChain(key) { // Abstract base class for terms export class TermBase { - constructor(sendRequest, query, legalMethods) { + constructor(capabilities, sendRequest, request) { this._sendRequest = sendRequest - this._query = query - this._legalMethods = legalMethods + this._request = request + + // TODO: do something fancy with prototypes? Might be more efficient + // this should be the same for all requests on the same instance of horizon + for (const name of capabilities) { + this[name] = (...args) => this._addOption(name, ...args) + } } toString() { @@ -72,6 +77,11 @@ export class TermBase { } return string } + + _addOption(name, ...args) { + return new + } + // Returns a sequence of the result set. Every time it changes the // updated sequence will be emitted. If raw change objects are // needed, pass the option 'rawChanges: true'. An observable is @@ -137,7 +147,29 @@ export class TermBase { // `observable` is the base observable with full responses coming from // the HorizonSocket // `query` is the value of `options` in the request -function makePresentable(observable, query) { +function makePresentable(observable) { + const initialAcc = {state: 'initial', type}; + return observable.scan((acc, message) => { + switch (acc.state) { + case 'initial': + if (message.data) { + message.data.forEach((x) => applyChange( + } + break; + case 'synced': + break; + case 'complete': + break; + } + if (message + switch (change.state) { + case 'synced': + break; + case 'complete': + break; + } + }, initialAcc).filter((acc) => acc.state === 'synced').map((acc) => acc.data); + // Whether the entire data structure is in each change const pointQuery = Boolean(query.find) @@ -149,12 +181,6 @@ function makePresentable(observable, query) { .filter(change => !hasEmitted || change.type !== 'state') .scan((previous, change) => { hasEmitted = true - if (change.new_val != null) { - delete change.new_val.$hz_v$ - } - if (change.old_val != null) { - delete change.old_val.$hz_v$ - } if (change.state === 'synced') { return previous } else { @@ -165,12 +191,6 @@ function makePresentable(observable, query) { const seedVal = { emitted: false, val: [] } return observable .scan((state, change) => { - if (change.new_val != null) { - delete change.new_val.$hz_v$ - } - if (change.old_val != null) { - delete change.old_val.$hz_v$ - } if (change.state === 'synced') { state.emitted = true } diff --git a/plugin-router/base/.eslintrc.js b/plugin-router/.eslintrc.js similarity index 78% rename from plugin-router/base/.eslintrc.js rename to plugin-router/.eslintrc.js index 535101a8a..bc66210fa 100644 --- a/plugin-router/base/.eslintrc.js +++ b/plugin-router/.eslintrc.js @@ -5,7 +5,8 @@ const ERROR = 2; module.exports = { extends: "../.eslintrc.js", rules: { - "max-len": [ ERROR, 130 ], + "camelcase": [ ERROR ], + "max-len": [ ERROR, 89 ], "prefer-template": [ OFF ], }, env: { diff --git a/plugin-router/base/src/index.js b/plugin-router/base/src/index.js index 1ed89f374..809b40d1b 100644 --- a/plugin-router/base/src/index.js +++ b/plugin-router/base/src/index.js @@ -26,7 +26,7 @@ class PluginRouter extends EventEmitter { if (!options.name) { // RSI: make sure plugin names don't contain a '/' options.name = plugin.name; - } + } if (this.plugins.has(options.name)) { return Promise.reject( diff --git a/plugin-router/express/.eslintrc.js b/plugin-router/express/.eslintrc.js deleted file mode 100644 index 535101a8a..000000000 --- a/plugin-router/express/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -const OFF = 0; -const WARN = 1; -const ERROR = 2; - -module.exports = { - extends: "../.eslintrc.js", - rules: { - "max-len": [ ERROR, 130 ], - "prefer-template": [ OFF ], - }, - env: { - "es6": true, - "node": true, - "mocha": true, - }, -}; diff --git a/plugin-router/express/src/index.js b/plugin-router/express/src/index.js index c6b370a1e..33094413f 100644 --- a/plugin-router/express/src/index.js +++ b/plugin-router/express/src/index.js @@ -3,27 +3,29 @@ const HorizonServer = require('@horizon/server'); const PluginRouterBase = require('@horizon/plugin-router-base'); -module.exports = (hz) => { - // Using local variables so we can later be removed without leaking things +const express = require('express'); + +function PluginRouterExpress(hz) { + // Using local variables so it can later be removed without leaking things + let horizonServer = hz; let pluginRouter = new PluginRouterBase(horizonServer); let expressRouter = express.Router(); let routes = new Map(); // For recreating the router when a route is removed - let horizonServer = hz; - const middleware = function (req, res, next) { + function middleware(req, res, next) { if (expressRouter) { - expressRouter(req, res, next); + expressRouter(req, res, next); } else { next(); } - }; + } - const addHandler = function (path, handler) { - routes.set(path, active.http); - expressRouter.use(path, active.http); - }; + function addHandler(path, handler) { + routes.set(path, handler); + expressRouter.use(path, handler); + } - middleware.add = function (...args) { + function add(...args) { return Promise.resolve().then(() => { if (!pluginRouter) { throw new Error('PluginRouter has been closed.'); } return pluginRouter.add(...args); @@ -35,7 +37,7 @@ module.exports = (hz) => { }); } - middleware.remove = function (name, ...args) { + function remove(name, ...args) { return Promise.resolve().then(() => { if (!pluginRouter) { throw new Error('PluginRouter has been closed.'); } @@ -44,50 +46,57 @@ module.exports = (hz) => { if (PluginRouterBase.isValidName(name) && routes.has(path)) { routes.delete(`/${name}/`); expressRouter = express.Router(); - routes.forEach((handler, path) => { - expressRouter.use(path, handler); + routes.forEach((handler, route) => { + expressRouter.use(route, handler); }); } return pluginRouter.remove(...args); }); - }; + } let closePromise; - middleware.close = function (...args) { + function close(...args) { if (!closePromise) { expressRouter = null; horizonServer = null; + routes.clear(); + routes = null; closePromise = pluginRouter.close(...args); pluginRouter = null; } return closePromise; - }; + } - addHandler('/horizon.js', (req, res, next) => { + addHandler('/horizon.js', (req, res) => { res.head('Content-Type', 'application/javascript'); res.send(HorizonServer.clientSource()); res.end(); }); - addHandler('/horizon.js.map', (req, res, next) => { + addHandler('/horizon.js.map', (req, res) => { res.head('Content-Type', 'application/javascript'); res.send(HorizonServer.clientSourceMap()); res.end(); }); - addHandler('/horizon-core.js.map', (req, res, next) => { + addHandler('/horizon-core.js.map', (req, res) => { res.head('Content-Type', 'application/javascript'); res.send(HorizonServer.clientSourceCore()); res.end(); }); - addHandler('/horizon-core.js.map', (req, res, next) => { + addHandler('/horizon-core.js.map', (req, res) => { res.head('Content-Type', 'application/javascript'); res.send(HorizonServer.clientSourceCoreMap()); res.end(); }); + middleware.add = add; + middleware.remove = remove; + middleware.close = close; return middleware; -}; +} + +module.exports = PluginRouterExpress; diff --git a/plugin-router/express/src/request.js b/plugin-router/express/src/request.js deleted file mode 100644 index f6c2ff1f7..000000000 --- a/plugin-router/express/src/request.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -class Request { - constructor(request, currentMethod) { - this.request_id = request.request_id; - this.options = request.options; - this.clientCtx = request.clientCtx; - this._parameters = request._parameters; - this._currentMethod = currentMethod; - Object.freeze(this); - } - - getParameter(methodName) { - return this._parameters[methodName]; - } - - setParameter(value) { - assert(this._currentMethod); - return this._parameters[this._currentMethod] = value; - } -} - -module.exports = Request; diff --git a/plugin-router/hapi/.eslintrc.js b/plugin-router/hapi/.eslintrc.js deleted file mode 100644 index 535101a8a..000000000 --- a/plugin-router/hapi/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -const OFF = 0; -const WARN = 1; -const ERROR = 2; - -module.exports = { - extends: "../.eslintrc.js", - rules: { - "max-len": [ ERROR, 130 ], - "prefer-template": [ OFF ], - }, - env: { - "es6": true, - "node": true, - "mocha": true, - }, -}; diff --git a/plugin-router/hapi/src/index.js b/plugin-router/hapi/src/index.js index caf26be56..c643ffab6 100644 --- a/plugin-router/hapi/src/index.js +++ b/plugin-router/hapi/src/index.js @@ -9,19 +9,15 @@ class PluginRouterHapi extends PluginRouterBase { } add(...args) { - return super.add(...args).then((res) => { - // RSI: add routes to hapi - return res; - }); + return super.add(...args); + // RSI: add routes to hapi } remove(...args) { - return super.remove(...args).then((res) => { - // RSI: remove routes from hapi - return res; - }); + // RSI: remove routes from hapi + return super.remove(...args); } -}; +} module.exports = PluginRouterHapi; diff --git a/plugin-router/hapi/src/request.js b/plugin-router/hapi/src/request.js deleted file mode 100644 index f6c2ff1f7..000000000 --- a/plugin-router/hapi/src/request.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -class Request { - constructor(request, currentMethod) { - this.request_id = request.request_id; - this.options = request.options; - this.clientCtx = request.clientCtx; - this._parameters = request._parameters; - this._currentMethod = currentMethod; - Object.freeze(this); - } - - getParameter(methodName) { - return this._parameters[methodName]; - } - - setParameter(value) { - assert(this._currentMethod); - return this._parameters[this._currentMethod] = value; - } -} - -module.exports = Request; diff --git a/plugin-router/http/.eslintrc.js b/plugin-router/http/.eslintrc.js deleted file mode 100644 index 535101a8a..000000000 --- a/plugin-router/http/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -const OFF = 0; -const WARN = 1; -const ERROR = 2; - -module.exports = { - extends: "../.eslintrc.js", - rules: { - "max-len": [ ERROR, 130 ], - "prefer-template": [ OFF ], - }, - env: { - "es6": true, - "node": true, - "mocha": true, - }, -}; diff --git a/plugin-router/http/src/index.js b/plugin-router/http/src/index.js index de053443d..07ee8c75d 100644 --- a/plugin-router/http/src/index.js +++ b/plugin-router/http/src/index.js @@ -9,18 +9,14 @@ class PluginRouterHttp extends PluginRouterBase { } add(...args) { - return super.add(...args).then((res) => { - // RSI: add routes to http server - return res; - }); + return super.add(...args); + // RSI: add routes to http server } remove(...args) { - return super.remove(...args).then((res) => { - // RSI: remove routes from http server - return res; - }); + // RSI: remove routes from http server + return super.remove(...args); } -}; +} module.exports = PluginRouterHttp; diff --git a/plugin-router/http/src/request.js b/plugin-router/http/src/request.js deleted file mode 100644 index f6c2ff1f7..000000000 --- a/plugin-router/http/src/request.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -class Request { - constructor(request, currentMethod) { - this.request_id = request.request_id; - this.options = request.options; - this.clientCtx = request.clientCtx; - this._parameters = request._parameters; - this._currentMethod = currentMethod; - Object.freeze(this); - } - - getParameter(methodName) { - return this._parameters[methodName]; - } - - setParameter(value) { - assert(this._currentMethod); - return this._parameters[this._currentMethod] = value; - } -} - -module.exports = Request; diff --git a/plugin-router/koa/.eslintrc.js b/plugin-router/koa/.eslintrc.js deleted file mode 100644 index 535101a8a..000000000 --- a/plugin-router/koa/.eslintrc.js +++ /dev/null @@ -1,16 +0,0 @@ -const OFF = 0; -const WARN = 1; -const ERROR = 2; - -module.exports = { - extends: "../.eslintrc.js", - rules: { - "max-len": [ ERROR, 130 ], - "prefer-template": [ OFF ], - }, - env: { - "es6": true, - "node": true, - "mocha": true, - }, -}; diff --git a/plugin-router/koa/src/index.js b/plugin-router/koa/src/index.js index 12cec4156..b8213b994 100644 --- a/plugin-router/koa/src/index.js +++ b/plugin-router/koa/src/index.js @@ -9,18 +9,14 @@ class PluginRouterKoa extends PluginRouterBase { } add(...args) { - return super.add(...args).then((res) => { - // RSI: add routes to koa server - return res; - }); + return super.add(...args); + // RSI: add routes to koa server } remove(...args) { - return super.remove(...args).then((res) => { - // RSI: remove routes from koa server - return res; - }); + // RSI: remove routes from koa server + return super.remove(...args); } -}; +} module.exports = PluginRouterKoa; diff --git a/plugin-router/koa/src/request.js b/plugin-router/koa/src/request.js deleted file mode 100644 index f6c2ff1f7..000000000 --- a/plugin-router/koa/src/request.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -class Request { - constructor(request, currentMethod) { - this.request_id = request.request_id; - this.options = request.options; - this.clientCtx = request.clientCtx; - this._parameters = request._parameters; - this._currentMethod = currentMethod; - Object.freeze(this); - } - - getParameter(methodName) { - return this._parameters[methodName]; - } - - setParameter(value) { - assert(this._currentMethod); - return this._parameters[this._currentMethod] = value; - } -} - -module.exports = Request; diff --git a/server/src/auth/utils.js b/plugin-utils/src/auth-utils.js similarity index 100% rename from server/src/auth/utils.js rename to plugin-utils/src/auth-utils.js diff --git a/plugin-utils/src/reads.js b/plugin-utils/src/reads.js index 90b7d58b7..8fcfb11cd 100644 --- a/plugin-utils/src/reads.js +++ b/plugin-utils/src/reads.js @@ -58,7 +58,7 @@ function hasField(fields, expected) { } function makeFindReql(collection, find) { - return collection.get_matching_index(objectToFields(find), []).then((index) => { + return collection.getMatchingIndex(objectToFields(find), []).then((index) => { let value = index.fields.map((field) => guaranteeObjectField(find, field)); if (index.name === 'id') { @@ -83,7 +83,7 @@ function makeFindAllReql(collection, findAll, fixedFields, above, below, descend // RSI: make sure fuzzyFields and fixedFields overlap only in the correct spot // RSI: come up with some pathological tests that hit these sorts of cases - return collection.get_matching_index(fuzzyFields, fixedFields).then((index) => { + return collection.getMatchingIndex(fuzzyFields, fixedFields).then((index) => { const optargs = { index: index.name, leftBound: above ? above.bound : 'closed', @@ -116,7 +116,7 @@ function makeFindAllReql(collection, findAll, fixedFields, above, below, descend } function makeTableScanReql(collection, fixedFields, above, below, descending) { - return collection.get_matching_index([], fixedFields).then((index) => { + return collection.getMatchingIndex([], fixedFields).then((index) => { let leftValue, rightValue; const optargs = {index: index.name}; diff --git a/plugins/.eslintrc.js b/plugins/.eslintrc.js index d5f233311..bc66210fa 100644 --- a/plugins/.eslintrc.js +++ b/plugins/.eslintrc.js @@ -5,6 +5,7 @@ const ERROR = 2; module.exports = { extends: "../.eslintrc.js", rules: { + "camelcase": [ ERROR ], "max-len": [ ERROR, 89 ], "prefer-template": [ OFF ], }, diff --git a/server/src/auth/auth0.js b/plugins/auth0/src/auth0.js similarity index 100% rename from server/src/auth/auth0.js rename to plugins/auth0/src/auth0.js diff --git a/plugins/collection/src/collection.js b/plugins/collection/src/collection.js index 5a88cd602..c37e1cb73 100644 --- a/plugins/collection/src/collection.js +++ b/plugins/collection/src/collection.js @@ -19,6 +19,11 @@ function collection(metadata) { // Wait up to 5 seconds for metadata readiness // This should only happen if the connection to the database just recovered, // or there is some invalid data in the metadata. + const timer = setTimeout(() => { + reject(new Error('Timed out waiting for metadata ' + + 'to sync with the database.')); + }, 5000); + const subs = metadata.subscribe({ onReady: () => { subs.close(); @@ -26,11 +31,6 @@ function collection(metadata) { resolve(); }, }); - - const timer = setTimeout(() => { - reject(new Error('Timed out waiting for metadata ' + - 'to sync with the database.')); - }, 5000); } }).then(() => metadata.collection(args[0])).then((c) => { req.setParameter(c); @@ -48,7 +48,7 @@ module.exports = { Boolean(options.auto_create_collection), Boolean(options.auto_create_index)); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { context[options.name].subscribe({onUnready, onReady: () => { resolve({ methods: { diff --git a/plugins/collection/src/indexes.js b/plugins/collection/src/indexes.js index c7af3a78b..b30f842af 100644 --- a/plugins/collection/src/indexes.js +++ b/plugins/collection/src/indexes.js @@ -23,7 +23,7 @@ function indexNameToInfo(name) { const matches = name.match(re); assert(matches !== null, `Unexpected index name (invalid format): "${name}"`); - const json_offset = matches[0].length - 1; + const jsonOffset = matches[0].length - 1; const info = { name, @@ -33,13 +33,13 @@ function indexNameToInfo(name) { // Parse remainder as JSON try { - info.fields = JSON.parse(name.slice(json_offset)); + info.fields = JSON.parse(name.slice(jsonOffset)); } catch (err) { assert(false, `Unexpected index name (invalid JSON): "${name}"`); } // Sanity check fields - const validate_field = (f) => { + const validateField = (f) => { assert(Array.isArray(f), `Unexpected index name (invalid field): "${name}"`); f.forEach((s) => assert(typeof s === 'string', `Unexpected index name (invalid field): "${name}"`)); @@ -49,7 +49,7 @@ function indexNameToInfo(name) { `Unexpected index name (fields are not an array): "${name}"`); assert((info.multi === false) || (info.multi < info.fields.length), `Unexpected index name (multi index out of bounds): "${name}"`); - info.fields.forEach(validate_field); + info.fields.forEach(validateField); return info; } @@ -71,14 +71,14 @@ function indexInfoToReql(info) { } if (info.multi !== false) { - const multi_field = info.fields[info.multi]; + const multiField = info.fields[info.multi]; return (row) => - row(multi_field).map((value) => info.fields.map((f, i) => { + row(multiField).map((value) => info.fields.map((f, i) => { if (i === info.multi) { return value; } else { let res = row; - f.forEach((field_name) => { res = res(field_name); }); + f.forEach((fieldName) => { res = res(fieldName); }); return res; } })); @@ -86,7 +86,7 @@ function indexInfoToReql(info) { return (row) => info.fields.map((f) => { let res = row; - f.forEach((field_name) => { res = res(field_name); }); + f.forEach((fieldName) => { res = res(fieldName); }); return res; }); } diff --git a/plugins/collection/src/types/collection.js b/plugins/collection/src/types/collection.js index d45eb45af..63379bb79 100644 --- a/plugins/collection/src/types/collection.js +++ b/plugins/collection/src/types/collection.js @@ -24,20 +24,20 @@ class Collection { this._waiters = []; } - _update_table(table_id, indexes, conn) { - let table = this._tables.get(table_id); + _updateTable(tableId, indexes, conn) { + let table = this._tables.get(tableId); if (indexes) { if (!table) { table = new Table(this.table, conn); - this._tables.set(table_id, table); + this._tables.set(tableId, table); } - table.update_indexes(indexes, conn); - this._waiters.forEach((w) => table.on_ready(w)); + table.updateIndexes(indexes, conn); + this._waiters.forEach((w) => table.onReady(w)); this._waiters = []; } else { - this._tables.delete(table_id); + this._tables.delete(tableId); if (table) { - table._waiters.forEach((w) => this.on_ready(w)); + table._waiters.forEach((w) => this.onReady(w)); table._waiters = []; table.close(); } @@ -52,37 +52,37 @@ class Collection { this._registered = false; } - _is_safe_to_remove() { + _isSafeToRemove() { return this._tables.size === 0 && !this._registered; } - _on_ready(done) { + _onReady(done) { if (this._tables.size === 0) { this._waiters.push(done); } else { - this._get_table().on_ready(done); + this._getTable().onReady(done); } } - _get_table() { + _getTable() { if (this._tables.size === 0) { throw new Error(`Collection ${this.name} is not ready.`); } return this._tables.values().next().value; } - _create_index(fields, done) { - return this._get_table().create_index(fields, this.reliableConn.connection(), done); + _createIndex(fields, done) { + return this._getTable().createIndex(fields, this.reliableConn.connection(), done); } ready() { if (this._tables.size === 0) { return false; } - return this._get_table().ready(); + return this._getTable().ready(); } - get_matching_index(fuzzy_fields, ordered_fields) { + getMatchingIndex(fuzzyFields, orderedFields) { return new Promise((resolve, reject) => { const done = (indexOrErr) => { if (indexOrErr instanceof Error) { @@ -92,15 +92,15 @@ class Collection { } }; - const match = this._get_table().get_matching_index(fuzzy_fields, ordered_fields); + const match = this._getTable().getMatchingIndex(fuzzyFields, orderedFields); if (match) { if (match.ready()) { resolve(match); } else { - match.on_ready(done); + match.onReady(done); } } else { - this._create_index(fuzzy_fields.concat(ordered_fields), done); + this._createIndex(fuzzyFields.concat(orderedFields), done); } }); } diff --git a/plugins/collection/src/types/index.js b/plugins/collection/src/types/index.js index 754c86d25..8d2bbe281 100644 --- a/plugins/collection/src/types/index.js +++ b/plugins/collection/src/types/index.js @@ -4,7 +4,7 @@ const {indexNameToInfo, primaryIndexName} = require('../indexes'); const {logger} = require('@horizon/server'); -const compare_fields = (a, b) => { +const compareFields = (a, b) => { if (a.length !== b.length) { return false; } @@ -60,7 +60,7 @@ class Index { return this._result === true; } - on_ready(done) { + onReady(done) { if (this._result === true) { done(); } else if (this._result) { @@ -70,34 +70,34 @@ class Index { } } - // `fuzzy_fields` may be in any order at the beginning of the index. - // These must be immediately followed by `ordered_fields` in the exact + // `fuzzyFields` may be in any order at the beginning of the index. + // These must be immediately followed by `orderedFields` in the exact // order given. There may be no other fields present in the index // (because the absence of a field would mean that row is not indexed). - // `fuzzy_fields` may overlap with `ordered_fields`. - is_match(fuzzy_fields, ordered_fields) { + // `fuzzyFields` may overlap with `orderedFields`. + isMatch(fuzzyFields, orderedFields) { // TODO: multi index matching if (this.geo || this.multi !== false) { return false; } - if (this.fields.length > fuzzy_fields.length + ordered_fields.length || - this.fields.length < fuzzy_fields.length || - this.fields.length < ordered_fields.length) { + if (this.fields.length > fuzzyFields.length + orderedFields.length || + this.fields.length < fuzzyFields.length || + this.fields.length < orderedFields.length) { return false; } - for (let i = 0; i < fuzzy_fields.length; ++i) { + for (let i = 0; i < fuzzyFields.length; ++i) { let found = false; - for (let j = 0; j < fuzzy_fields.length && !found; ++j) { - found = compare_fields(fuzzy_fields[i], this.fields[j]); + for (let j = 0; j < fuzzyFields.length && !found; ++j) { + found = compareFields(fuzzyFields[i], this.fields[j]); } if (!found) { return false; } } - for (let i = 0; i < ordered_fields.length; ++i) { - const pos = this.fields.length - ordered_fields.length + i; - if (pos < 0 || !compare_fields(ordered_fields[i], this.fields[pos])) { + for (let i = 0; i < orderedFields.length; ++i) { + const pos = this.fields.length - orderedFields.length + i; + if (pos < 0 || !compareFields(orderedFields[i], this.fields[pos])) { return false; } } diff --git a/plugins/collection/src/types/metadata.js b/plugins/collection/src/types/metadata.js index 291590370..4a30af265 100644 --- a/plugins/collection/src/types/metadata.js +++ b/plugins/collection/src/types/metadata.js @@ -19,17 +19,17 @@ class StaleAttemptError extends Error { } // RSI: fix all this shit. class ReliableInit extends Reliable { - constructor(db, reliable_conn, auto_create_collection) { + constructor(db, rdbConnection, auto_create_collection) { super(); - this._db = db; - this._auto_create_collection = auto_create_collection; - this._conn_subs = reliable_conn.subscribe({ + this.db = db; + this.auto_create_collection = auto_create_collection; + this.connSubs = rdbConnection.subscribe({ onReady: (conn) => { - this.current_attempt = Symbol(); - this.do_init(conn, this.current_attempt); + this.currentAttempt = Symbol(); + this.doInit(conn, this.currentAttempt); }, onUnready: () => { - this.current_attempt = null; + this.currentAttempt = null; if (this.ready) { this.emit('onUnready'); } @@ -37,73 +37,73 @@ class ReliableInit extends Reliable { }); } - check_attempt(attempt) { - if (attempt !== this.current_attempt) { + checkAttempt(attempt) { + if (attempt !== this.currentAttempt) { throw new StaleAttemptError(); } } - do_init(conn, attempt) { + doInit(conn, attempt) { Promise.resolve().then(() => { - this.check_attempt(attempt); + this.checkAttempt(attempt); logger.debug('checking rethinkdb version'); const q = r.db('rethinkdb').table('server_status').nth(0)('process')('version'); return q.run(conn).then((res) => rethinkdbVersionCheck(res)); }).then(() => { - this.check_attempt(attempt); + this.checkAttempt(attempt); logger.debug('checking for old metadata version'); - const old_metadata_db = `${this._db}_internal`; - return r.dbList().contains(old_metadata_db).run(conn).then((has_old_db) => { - if (has_old_db) { + const oldMetadataDb = `${this.db}_internal`; + return r.dbList().contains(oldMetadataDb).run(conn).then((hasOldDb) => { + if (hasOldDb) { throw new Error('The Horizon metadata appears to be from v1.x because ' + - `the "${old_metadata_db}" database exists. Please use ` + + `the "${oldMetadataDb}" database exists. Please use ` + '`hz migrate` to convert your metadata to the new format.'); } }); }).then(() => { - this.check_attempt(attempt); + this.checkAttempt(attempt); logger.debug('checking for internal tables'); - if (this._auto_create_collection) { - return queries.initializeMetadata(this._db, conn); + if (this.auto_create_collection) { + return queries.initializeMetadata(this.db, conn); } else { - return r.dbList().contains(this._db).run(conn).then((has_db) => { - if (!has_db) { - throw new Error(`The database ${this._db} does not exist. ` + + return r.dbList().contains(this.db).run(conn).then((hasDb) => { + if (!hasDb) { + throw new Error(`The database ${this.db} does not exist. ` + 'Run `hz schema apply` to initialize the database, ' + 'then start the Horizon server.'); } }); } }).then(() => { - this.check_attempt(attempt); + this.checkAttempt(attempt); logger.debug('waiting for internal tables'); return r.expr(['hz_collections', 'hz_users_auth', 'hz_groups', 'users']) - .forEach((table) => r.db(this._db).table(table).wait({timeout: 30})).run(conn); + .forEach((table) => r.db(this.db).table(table).wait({timeout: 30})).run(conn); }).then(() => { - this.check_attempt(attempt); + this.checkAttempt(attempt); logger.debug('adding admin user'); return Promise.all([ - r.db(this._db).table('users').get('admin') - .replace((old_row) => - r.branch(old_row.eq(null), + r.db(this.db).table('users').get('admin') + .replace((oldRow) => + r.branch(oldRow.eq(null), { id: 'admin', groups: ['admin'], }, - old_row), + oldRow), {returnChanges: 'always'})('changes')(0) .do((res) => r.branch(res('new_val').eq(null), r.error(res('error')), res('new_val'))).run(conn), - r.db(this._db).table('hz_groups').get('admin') - .replace((old_row) => - r.branch(old_row.eq(null), + r.db(this.db).table('hz_groups').get('admin') + .replace((oldRow) => + r.branch(oldRow.eq(null), { id: 'admin', rules: {carte_blanche: {template: 'any()'}}, }, - old_row), + oldRow), {returnChanges: 'always'})('changes')(0) .do((res) => r.branch(res('new_val').eq(null), @@ -111,21 +111,21 @@ class ReliableInit extends Reliable { res('new_val'))).run(conn), ]); }).then(() => { - this.check_attempt(attempt); + this.checkAttempt(attempt); logger.debug('metadata sync complete'); this.emit('onReady'); }).catch((err) => { if (!(err instanceof StaleAttemptError)) { logger.debug(`Metadata initialization failed: ${err.stack}`); - setTimeout(() => { this.do_init(conn, attempt); }, 1000); + setTimeout(() => { this.doInit(conn, attempt); }, 1000); } }); } close(reason) { - this.current_attempt = null; + this.currentAttempt = null; super.close(reason); - this._conn_subs.close(reason); + this.connSubs.close(reason); } } @@ -134,31 +134,31 @@ class ReliableMetadata extends Reliable { auto_create_collection, auto_create_index) { super(); - this._db = context.horizon.options.project_name; - this._reliable_conn = context.horizon.rdbConnection; - this._auto_create_collection = auto_create_collection; - this._auto_create_index = auto_create_index; - this._collections = new Map(); + this.db = context.horizon.options.project_name; + this.rdbConnection = context.horizon.rdbConnection; + this.auto_create_collection = auto_create_collection; + this.auto_create_index = auto_create_index; + this.collections = new Map(); - this._reliable_init = new ReliableInit( - this._db, this._reliable_conn, auto_create_collection); + this.reliableInit = new ReliableInit( + this.db, this.rdbConnection, auto_create_collection); - this._conn_subscription = this._reliable_conn.subscribe({ + this.connSubs = this.rdbConnection.subscribe({ onReady: (conn) => { - this._connection = conn; + this.connection = conn; }, onUnready: () => { - this._connection = null; + this.connection = null; }, }); // RSI: stop these from running until after ReliableInit? - this._collection_changefeed = new ReliableChangefeed( - r.db(this._db) + this.collectionChangefeed = new ReliableChangefeed( + r.db(this.db) .table('hz_collections') .filter((row) => row('id').match('^hzp?_').not()) .changes({squash: false, includeInitial: true, includeTypes: true}), - this._reliable_conn, + this.rdbConnection, { onChange: (change) => { switch (change.type) { @@ -166,12 +166,12 @@ class ReliableMetadata extends Reliable { case 'add': case 'change': { - const collection_name = change.new_val.id; - let collection = this._collections.get(collection_name); + const name = change.new_val.id; + let collection = this.collections.get(name); if (!collection) { collection = new Collection( - this._db, collection_name, this._reliable_conn); - this._collections.set(collection_name, collection); + this.db, name, this.rdbConnection); + this.collections.set(name, collection); } collection._register(); } @@ -179,12 +179,12 @@ class ReliableMetadata extends Reliable { case 'uninitial': case 'remove': { - const collection_name = change.new_val.id; - const collection = this._collections.get(collection_name); + const name = change.new_val.id; + const collection = this.collections.get(name); if (collection) { collection._unregister(); - if (collection._is_safe_to_remove()) { - this._collections.delete(collection_name); + if (collection._isSafeToRemove()) { + this.collections.delete(name); collection.close(); } } @@ -197,10 +197,10 @@ class ReliableMetadata extends Reliable { }, }); - this._index_changefeed = new ReliableChangefeed( + this.indexChangefeed = new ReliableChangefeed( r.db('rethinkdb') .table('table_config') - .filter((row) => r.and(row('db').eq(this._db), + .filter((row) => r.and(row('db').eq(this.db), row('name').match('^hzp?_').not())) .map((row) => ({ id: row('id'), @@ -208,36 +208,36 @@ class ReliableMetadata extends Reliable { indexes: row('indexes').filter((idx) => idx.match('^hz_')), })) .changes({squash: true, includeInitial: true, includeTypes: true}), - this._reliable_conn, + this.rdbConnection, { onChange: (change) => { - if (!this._connection) { return; } + if (!this.connection) { return; } switch (change.type) { case 'initial': case 'add': case 'change': { - const collection_name = change.new_val.name; - const table_id = change.new_val.id; + const name = change.new_val.name; + const tableId = change.new_val.id; - let collection = this._collections.get(collection_name); + let collection = this.collections.get(name); if (!collection) { collection = new Collection( - this._db, collection_name, this._reliable_conn); - this._collections.set(collection_name, collection); + this.db, name, this.rdbConnection); + this.collections.set(name, collection); } - collection._update_table( - table_id, change.new_val.indexes, this._connection); + collection._updateTable( + tableId, change.new_val.indexes, this.connection); } break; case 'uninitial': case 'remove': { - const collection = this._collections.get(change.old_val.name); + const collection = this.collections.get(change.old_val.name); if (collection) { - collection._update_table(change.old_val.id, null, this._connection); - if (collection._is_safe_to_remove()) { - this._collections.delete(collection); + collection._updateTable(change.old_val.id, null, this.connection); + if (collection._isSafeToRemove()) { + this.collections.delete(collection); collection.close(); } } @@ -250,10 +250,10 @@ class ReliableMetadata extends Reliable { }, }); - this._ready_union = new ReliableUnion({ - reliable_init: this._reliable_init, - collection_changefeed: this._collection_changefeed, - index_changefeed: this._index_changefeed, + this.readyUnion = new ReliableUnion({ + reliableInit: this.reliableInit, + collectionChangefeed: this.collectionChangefeed, + indexChangefeed: this.indexChangefeed, }, { onReady: () => { this.emit('onReady'); @@ -261,8 +261,8 @@ class ReliableMetadata extends Reliable { onUnready: () => { // TODO: fill in the reason for `close`. this.emit('onUnready'); - this._collections.forEach((collection) => collection.close()); - this._collections.clear(); + this.collections.forEach((collection) => collection.close()); + this.collections.clear(); }, }); } @@ -270,10 +270,11 @@ class ReliableMetadata extends Reliable { close(reason) { return Promise.all([ super.close(reason), - this._reliable_init.close(reason), - this._collection_changefeed.close(reason), - this._index_changefeed.close(reason), - this._ready_union.close(reason), + this.connSubs.close(reason), + this.reliableInit.close(reason), + this.collectionChangefeed.close(reason), + this.indexChangefeed.close(reason), + this.readyUnion.close(reason), ]); } @@ -288,13 +289,13 @@ class ReliableMetadata extends Reliable { throw new Error('Metadata is not synced with the database.'); } - const collection = this._collections.get(name); - if (!collection && !this._auto_create_collection) { + const collection = this.collections.get(name); + if (!collection && !this.auto_create_collection) { throw new Error(`Collection "${name}" does not exist.`); } else if (collection) { if (!collection.ready()) { return new Promise((resolve, reject) => - collection._on_ready((maybeErr) => { + collection._onReady((maybeErr) => { if (maybeErr instanceof Error) { reject(maybeErr); } else { @@ -304,11 +305,11 @@ class ReliableMetadata extends Reliable { } return collection; } - return this.create_collection(name); + return this.createCollection(name); }); } - create_collection(name) { + createCollection(name) { let collection; return Promise.resolve().then(() => { if (name.indexOf('hz_') === 0 || name.indexOf('hzp_') === 0) { @@ -316,19 +317,19 @@ class ReliableMetadata extends Reliable { 'and cannot be used in requests.'); } else if (!this.ready) { throw new Error('Metadata is not synced with the database.'); - } else if (this._collections.get(name)) { + } else if (this.collections.get(name)) { throw new Error(`Collection "${name}" already exists.`); } - collection = new Collection(this._db, name, this._reliable_conn); - this._collections.set(name, collection); + collection = new Collection(this.db, name, this.rdbConnection); + this.collections.set(name, collection); - return queries.createCollection(this._db, name, this._reliable_conn.connection()); + return queries.createCollection(this.db, name, this.rdbConnection.connection()); }).then((res) => { assert(!res.error, `Collection "${name}" creation failed: ${res.error}`); logger.warn(`Collection created: "${name}"`); return new Promise((resolve, reject) => - collection._on_ready((maybeErr) => { + collection._onReady((maybeErr) => { if (maybeErr instanceof Error) { reject(maybeErr); } else { @@ -336,8 +337,8 @@ class ReliableMetadata extends Reliable { } })); }).catch((err) => { - if (collection && collection._is_safe_to_remove()) { - this._collections.delete(name); + if (collection && collection._isSafeToRemove()) { + this.collections.delete(name); collection.close(); } throw err; diff --git a/plugins/collection/src/types/table.js b/plugins/collection/src/types/table.js index 08e64e86e..369fa6b00 100644 --- a/plugins/collection/src/types/table.js +++ b/plugins/collection/src/types/table.js @@ -8,8 +8,8 @@ const assert = require('assert'); const {r, logger} = require('@horizon/server'); class Table { - constructor(reql_table, conn) { - this.table = reql_table; + constructor(reqlTable, conn) { + this.table = reqlTable; this.indexes = new Map(); this._waiters = []; @@ -41,7 +41,7 @@ class Table { return this._result === true; } - on_ready(done) { + onReady(done) { if (this._result === true) { done(); } else if (this._result) { @@ -51,49 +51,49 @@ class Table { } } - update_indexes(indexes, conn) { + updateIndexes(indexes, conn) { logger.debug(`${this.table} indexes changed, reevaluating`); // Initialize the primary index, which won't show up in the changefeed indexes.push(primaryIndexName); - const new_index_map = new Map(); + const newIndexMap = new Map(); indexes.forEach((name) => { try { - const old_index = this.indexes.get(name); - const new_index = new Index(name, this.table, conn); - if (old_index) { + const oldIndex = this.indexes.get(name); + const newIndex = new Index(name, this.table, conn); + if (oldIndex) { // Steal any waiters from the old index - new_index._waiters = old_index._waiters; - old_index._waiters = []; + newIndex._waiters = oldIndex._waiters; + oldIndex._waiters = []; } - new_index_map.set(name, new_index); + newIndexMap.set(name, newIndex); } catch (err) { logger.warn(`${err}`); } }); this.indexes.forEach((i) => i.close()); - this.indexes = new_index_map; + this.indexes = newIndexMap; logger.debug(`${this.table} indexes updated`); } // TODO: support geo and multi indexes - create_index(fields, conn, done) { + createIndex(fields, conn, done) { const info = {geo: false, multi: false, fields}; - const index_name = indexInfoToName(info); - assert(!this.indexes.get(index_name), 'index already exists'); + const indexName = indexInfoToName(info); + assert(!this.indexes.get(indexName), 'index already exists'); const success = () => { // Create the Index object now so we don't try to create it again before the // feed notifies us of the index creation - const new_index = new Index(index_name, this.table, conn); + const newIndex = new Index(indexName, this.table, conn); // TODO: shouldn't this be done before we go async? - this.indexes.set(index_name, new_index); - return new_index.on_ready(done); + this.indexes.set(indexName, newIndex); + return newIndex.onReady(done); }; - this.table.indexCreate(index_name, indexInfoToReql(info), + this.table.indexCreate(indexName, indexInfoToReql(info), {geo: info.geo, multi: (info.multi !== false)}) .run(conn) .then(success) @@ -108,15 +108,15 @@ class Table { } // Returns a matching (possibly compound) index for the given fields - // fuzzy_fields and ordered_fields should both be arrays - get_matching_index(fuzzy_fields, ordered_fields) { - if (fuzzy_fields.length === 0 && ordered_fields.length === 0) { + // `fuzzyFields` and `orderedFields` should both be arrays + getMatchingIndex(fuzzyFields, orderedFields) { + if (fuzzyFields.length === 0 && orderedFields.length === 0) { return this.indexes.get(primaryIndexName); } let match; for (const i of this.indexes.values()) { - if (i.is_match(fuzzy_fields, ordered_fields)) { + if (i.isMatch(fuzzyFields, orderedFields)) { if (i.ready()) { return i; } else if (!match) { diff --git a/server/src/auth/facebook.js b/plugins/facebook/src/facebook.js similarity index 100% rename from server/src/auth/facebook.js rename to plugins/facebook/src/facebook.js diff --git a/server/src/auth/github.js b/plugins/github/src/github.js similarity index 100% rename from server/src/auth/github.js rename to plugins/github/src/github.js diff --git a/server/src/auth/google.js b/plugins/google/src/google.js similarity index 100% rename from server/src/auth/google.js rename to plugins/google/src/google.js diff --git a/plugins/insert/src/insert.js b/plugins/insert/src/insert.js index e4d285246..8e0584fa1 100644 --- a/plugins/insert/src/insert.js +++ b/plugins/insert/src/insert.js @@ -16,17 +16,17 @@ function insert(context) { throw new Error('No permissions given for insert operation.'); } - writes.retry_loop(request.options.insert, permissions, timeout, + writes.retryLoop(request.options.insert, permissions, timeout, (rows) => // pre-validation, all rows Array(rows.length).fill(null), (validator, row, info) => { // validation, each row if (!validator(info, row)) { - return new Error(writes.unauthorized_msg); + return new Error(writes.unauthorizedMsg); } }, (rows) => // write to database, all valid rows collection.table - .insert(rows.map((row) => writes.apply_version(r.expr(row), 0)), + .insert(rows.map((row) => writes.applyVersion(r.expr(row), 0)), {returnChanges: 'always'}) .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/permissions/src/group.js b/plugins/permissions/src/group.js index b342afb7b..f702ebc21 100644 --- a/plugins/permissions/src/group.js +++ b/plugins/permissions/src/group.js @@ -3,10 +3,10 @@ const Rule = require('./rule'); class Group { - constructor(row_data) { - this.name = row_data.id; - this.rules = Object.keys(row_data.rules).map((name) => - new Rule(name, row_data.rules[name])); + constructor(rowData) { + this.name = rowData.id; + this.rules = Object.keys(rowData.rules).map((name) => + new Rule(name, rowData.rules[name])); } } diff --git a/plugins/permissions/src/permissions.js b/plugins/permissions/src/permissions.js index ce7f6de04..02eac20f4 100644 --- a/plugins/permissions/src/permissions.js +++ b/plugins/permissions/src/permissions.js @@ -8,10 +8,6 @@ const Joi = require('joi'); const {r, logger, Reliable, ReliableChangefeed} = require('@horizon/server'); -// We can be desynced from the database for up to 5 seconds before we -// start rejecting queries. -const staleMs = 5000; - // auth plugins should set 'request.context.user' // token/anonymous: this should be the user id // unauthenticated: this should be null, and will use the default rules @@ -182,7 +178,7 @@ class UserCache { refcount: 1, unreadyAt: null, userRow: {id: null, groups: ['default']}, - readyPromise: Promise.resolve() + readyPromise: Promise.resolve(), })); this.queuedGroups = new Map(); @@ -246,9 +242,9 @@ class UserCache { } close() { - let promises = [this.groupCfeed.close()]; + const promises = [this.groupCfeed.close()]; this.userCfeeds.forEach((feed) => { - promises.push(feed.close()); + promises.push(feed.close()); }); return Promise.all(promises); } @@ -334,6 +330,8 @@ class UserCache { } } +// `cacheTimeout` - the duration we can be desynced from the database before we +// start rejecting queries. const optionsSchema = Joi.object().keys({ name: Joi.any().required(), usersTable: Joi.string().default('users'), @@ -347,7 +345,7 @@ module.exports = { activate(context, rawOptions, onReady, onUnready) { const options = Joi.attempt(rawOptions, optionsSchema); const userSub = Symbol(`${options.name}_userSub`); - + // Save things in the context that we will need at deactivation const userCache = new UserCache(context, options); context[options.name] = { @@ -365,7 +363,7 @@ module.exports = { context.horizon.events.on('auth', context[options.name].authCb); context.horizon.events.on('disconnect', context[options.name].disconnectCb); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { userCache.groupCfeed.subscribe({onUnready, onReady: () => { resolve({ methods: { @@ -388,7 +386,6 @@ module.exports = { onReady(); }}); }); - }, deactivate(context, options) { diff --git a/plugins/permissions/src/rule.js b/plugins/permissions/src/rule.js index 10deee839..cb2202a58 100644 --- a/plugins/permissions/src/rule.js +++ b/plugins/permissions/src/rule.js @@ -12,7 +12,7 @@ class Rule { } isMatch(query, context) { - return this.template.is_match(query, context); + return this.template.isMatch(query, context); } isValid(...args) { diff --git a/plugins/permissions/src/template.js b/plugins/permissions/src/template.js index cc5daab0e..341b407a2 100644 --- a/plugins/permissions/src/template.js +++ b/plugins/permissions/src/template.js @@ -4,6 +4,9 @@ const assert = require('assert'); const vm = require('vm'); // RSI: don't use the client AST - there are simple rules for generating options +// RSI: where do we get the list of options from? there's no easy way to accept any +// method - we could try to parse the ast of the javascript itself before evaluating +// the template const ast = require('@horizon/client/lib/ast'); const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; const {remakeError} = require('@horizon/plugin-utils'); @@ -21,7 +24,7 @@ class Any { } for (const item of this._values) { - if (template_compare(value, item, context)) { + if (templateCompare(value, item, context)) { return true; } } @@ -43,7 +46,7 @@ class AnyObject { } for (const key in this._obj) { - if (!template_compare(value[key], this._obj[key], context)) { + if (!templateCompare(value[key], this._obj[key], context)) { return false; } } @@ -67,7 +70,7 @@ class AnyArray { for (const item of value) { let match = false; for (const template of this._values) { - if (template_compare(item, template, context)) { + if (templateCompare(item, template, context)) { match = true; break; } @@ -83,7 +86,7 @@ class AnyArray { class UserId { } -const wrap_write = (query, docs) => { +const wrapWrite = (query, docs) => { if (docs instanceof AnyArray || Array.isArray(docs)) { query.data = docs; @@ -93,7 +96,7 @@ const wrap_write = (query, docs) => { return query; }; -const wrap_remove = (doc) => { +const wrapRemove = (doc) => { if (validIndexValue(doc)) { return {id: doc}; } @@ -113,7 +116,7 @@ ast.Collection.prototype.anyWrite = function() { } return this._sendRequest( new Any(['store', 'upsert', 'insert', 'replace', 'update', 'remove']), - wrap_write(new AnyObject(this._query), docs)); + wrapWrite(new AnyObject(this._query), docs)); }; // Monkey-patch the ast functions so we don't clobber certain things @@ -124,26 +127,26 @@ ast.TermBase.prototype.fetch = function() { return this._sendRequest('query', this._query); }; ast.Collection.prototype.store = function(docs) { - return this._sendRequest('store', wrap_write(this._query, docs)); + return this._sendRequest('store', wrapWrite(this._query, docs)); }; ast.Collection.prototype.upsert = function(docs) { - return this._sendRequest('upsert', wrap_write(this._query, docs)); + return this._sendRequest('upsert', wrapWrite(this._query, docs)); }; ast.Collection.prototype.insert = function(docs) { - return this._sendRequest('insert', wrap_write(this._query, docs)); + return this._sendRequest('insert', wrapWrite(this._query, docs)); }; ast.Collection.prototype.replace = function(docs) { - return this._sendRequest('replace', wrap_write(this._query, docs)); + return this._sendRequest('replace', wrapWrite(this._query, docs)); }; ast.Collection.prototype.update = function(docs) { - return this._sendRequest('update', wrap_write(this._query, docs)); + return this._sendRequest('update', wrapWrite(this._query, docs)); }; ast.Collection.prototype.remove = function(doc) { - return this._sendRequest('remove', wrap_write(this._query, wrap_remove(doc))); + return this._sendRequest('remove', wrapWrite(this._query, wrapRemove(doc))); }; ast.Collection.prototype.removeAll = function(docs) { - return this._sendRequest('remove', wrap_write(this._query, - docs.map((doc) => wrap_remove(doc)))); + return this._sendRequest('remove', wrapWrite(this._query, + docs.map((doc) => wrapRemove(doc)))); }; const env = { @@ -157,7 +160,7 @@ const env = { userId: function() { return new UserId(); }, }; -const make_template = (str) => { +const makeTemplate = (str) => { try { const sandbox = Object.assign({}, env); return vm.runInNewContext(str, sandbox); @@ -167,7 +170,7 @@ const make_template = (str) => { }; // eslint-disable-next-line prefer-const -function template_compare(query, template, context) { +function templateCompare(query, template, context) { if (template === undefined) { return false; } else if (template instanceof Any || @@ -190,7 +193,7 @@ function template_compare(query, template, context) { return false; } for (let i = 0; i < template.length; ++i) { - if (!template_compare(query[i], template[i], context)) { + if (!templateCompare(query[i], template[i], context)) { return false; } } @@ -200,7 +203,7 @@ function template_compare(query, template, context) { } for (const key in query) { - if (!template_compare(query[key], template[key], context)) { + if (!templateCompare(query[key], template[key], context)) { return false; } } @@ -218,13 +221,13 @@ function template_compare(query, template, context) { return true; } -const incomplete_template_message = (str) => +const incompleteTemplateMsg = (str) => `Incomplete template "${str}", ` + 'consider adding ".fetch()", ".watch()", ".anyRead()", or ".anyWrite()"'; class Template { constructor(str) { - this._value = make_template(str); + this._value = makeTemplate(str); assert(this._value !== null, `Invalid template: ${str}`); assert(!Array.isArray(this._value), `Invalid template: ${str}`); assert(typeof this._value === 'object', `Invalid template: ${str}`); @@ -235,14 +238,14 @@ class Template { this._value.anyRead) { this._value = this._value.anyRead(); } - assert(this._value.request_id !== undefined, incomplete_template_message(str)); - assert(this._value.type !== undefined, incomplete_template_message(str)); - assert(this._value.options !== undefined, incomplete_template_message(str)); + assert(this._value.request_id !== undefined, incompleteTemplateMsg(str)); + assert(this._value.type !== undefined, incompleteTemplateMsg(str)); + assert(this._value.options !== undefined, incompleteTemplateMsg(str)); } } - is_match(raw_query, context) { - return template_compare(raw_query, this._value, context); + isMatch(queryOptions, context) { + return templateCompare(queryOptions, this._value, context); } } diff --git a/plugins/permissions/src/test/template.js b/plugins/permissions/src/test/template.js index d7a15b501..eff0107ff 100644 --- a/plugins/permissions/src/test/template.js +++ b/plugins/permissions/src/test/template.js @@ -3,11 +3,10 @@ require('source-map-support').install(); const Rule = require('../rule'); -const Validator = require('../validator'); const assert = require('assert'); -const make_request = (type, collection, options) => { +const makeRequest = (type, collection, options) => { if (collection !== null) { return {request_id: 5, type, options: Object.assign({collection}, options)}; } else { @@ -22,10 +21,10 @@ describe('Template', () => { const rule = new Rule({template: 'any()'}); const tests = [{ }, - {type: 'query', options: {collection: 'test'}}, - {fake: 'bar'}, - {options: { }}, - {type: 'query', options: {fake: 'baz'}}]; + {type: 'query', options: {collection: 'test'}}, + {fake: 'bar'}, + {options: { }}, + {type: 'query', options: {fake: 'baz'}}]; for (const t of tests) { assert(rule.isMatch(t, context)); @@ -36,191 +35,191 @@ describe('Template', () => { it('any read', () => { const rule = new Rule({template: 'collection(any()).anyRead()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('fake', 'test', { }), context)); - assert(!rule.isMatch(make_request('store', 'test', { }), context)); - assert(!rule.isMatch(make_request('query', null, { }), context)); - assert(rule.isMatch(make_request('query', 'fake', { }), context)); - assert(rule.isMatch(make_request('query', 'fake', {find: { }}), context)); - assert(rule.isMatch(make_request('query', 'test', {bar: 'baz'}), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }, { }]}), context)); - assert(!rule.isMatch(make_request('subscribe', null, { }), context)); - assert(rule.isMatch(make_request('subscribe', 'fake', { }), context)); - assert(rule.isMatch(make_request('subscribe', 'fake', {find: { }}), context)); - assert(rule.isMatch(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(rule.isMatch(make_request('subscribe', 'test', {find_all: [{ }, { }]}), context)); + assert(!rule.isMatch(makeRequest('fake', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('store', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('query', null, { }), context)); + assert(rule.isMatch(makeRequest('query', 'fake', { }), context)); + assert(rule.isMatch(makeRequest('query', 'fake', {find: { }}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {bar: 'baz'}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{ }, { }]}), context)); + assert(!rule.isMatch(makeRequest('subscribe', null, { }), context)); + assert(rule.isMatch(makeRequest('subscribe', 'fake', { }), context)); + assert(rule.isMatch(makeRequest('subscribe', 'fake', {find: { }}), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', {bar: 'baz'}), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', {find_all: [{ }, { }]}), context)); }); it('any read with collection', () => { const rule = new Rule({template: 'collection("test").anyRead()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'fake', { }), context)); - assert(rule.isMatch(make_request('query', 'test', { }), context)); - assert(rule.isMatch(make_request('query', 'test', { }), context)); - assert(rule.isMatch(make_request('query', 'test', { }), context)); - assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); - assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); - assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); - assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('query', 'fake', { }), context)); + assert(rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', { }), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', { }), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', { }), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', { }), context)); }); it('any read with order', () => { // TODO: allow for any number of fields in order const rule = new Rule({template: 'collection("test").order(any(), any()).anyRead()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'fake', {order: ['foo', 'ascending']}), context)); - assert(!rule.isMatch(make_request('query', 'test', { }), context)); - assert(!rule.isMatch(make_request('query', 'test', {order: ['baz']}), context)); - assert(!rule.isMatch(make_request('query', 'test', {order: ['baz', 'fake']}), context)); - assert(!rule.isMatch(make_request('query', 'test', {order: [['fake']]}), context)); - assert(rule.isMatch(make_request('query', 'test', {order: [['foo'], 'ascending']}), context)); - assert(rule.isMatch(make_request('query', 'test', {order: [['bar'], 'descending']}), context)); - assert(rule.isMatch(make_request('query', 'test', {order: [['baz'], 'fake']}), context)); - assert(rule.isMatch(make_request('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); - assert(rule.isMatch(make_request('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); + assert(!rule.isMatch(makeRequest('query', 'fake', {order: ['foo', 'ascending']}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {order: ['baz']}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {order: ['baz', 'fake']}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {order: [['fake']]}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {order: [['foo'], 'ascending']}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {order: [['bar'], 'descending']}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {order: [['baz'], 'fake']}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: { }, order: [['baz'], 'fake']}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{ }], order: [['baz'], 'fake']}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {fake: 'baz', order: [['baz'], 'fake']}), context)); }); it('any read with find', () => { const rule = new Rule({template: 'collection("test").find(any()).anyRead()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'fake', {find: { }}), context)); - assert(!rule.isMatch(make_request('query', 'test', { }), context)); - assert(rule.isMatch(make_request('query', 'test', {find: { }}), context)); - assert(rule.isMatch(make_request('query', 'test', {find: { }, fake: 'baz'}), context)); + assert(!rule.isMatch(makeRequest('query', 'fake', {find: { }}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: { }}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: { }, fake: 'baz'}), context)); }); it('any read with findAll', () => { // TODO: allow for any number of arguments in findAll const rule = new Rule({template: 'collection("test").findAll(any()).anyRead()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'fake', {find_all: { }}), context)); - assert(!rule.isMatch(make_request('query', 'test', { }), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }]}), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); + assert(!rule.isMatch(makeRequest('query', 'fake', {find_all: { }}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{ }]}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{ }], fake: 'baz'}), context)); }); it('single key in findAll', () => { const rule = new Rule({template: 'collection("test").findAll({ owner: userId() }).fetch()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'test', {find_all: { }}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: []}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id}, {other: context.id}]}), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: { }}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: true}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: []}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: context.id}, {other: context.id}]}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: context.id}]}), context)); }); it('multiple keys in findAll', () => { const rule = new Rule({template: 'collection("test").findAll({ owner: userId(), key: any() }).fetch()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'test', {find_all: { }}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: []}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{owner: context.id, key: 3}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: { }}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: true}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: []}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: (context.id + 1)}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: context.id, bar: 'baz'}]}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{owner: context.id, key: 3}]}), context)); }); it('multiple items in findAll', () => { const rule = new Rule({template: 'collection("test").findAll({ a: userId() }, { b: userId() })'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'test', {find_all: { }}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: true}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: []}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{bar: 'baz'}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: (context.id + 1)}, {b: context.id}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id, bar: 'baz'}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id, b: context.id}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{b: context.id}]}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id, bar: 'baz'}]}), context)); - assert(rule.isMatch(make_request('query', 'test', {find_all: [{a: context.id}, {b: context.id}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: { }}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: true}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: []}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{bar: 'baz'}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{a: (context.id + 1)}, {b: context.id}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{a: context.id, bar: 'baz'}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{a: context.id, b: context.id}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{a: context.id}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{b: context.id}]}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find_all: [{a: context.id}, {b: context.id, bar: 'baz'}]}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find_all: [{a: context.id}, {b: context.id}]}), context)); }); it('collection fetch', () => { const rule = new Rule({template: 'collection("test").fetch()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'fake', { }), context)); - assert(!rule.isMatch(make_request('query', 'test', {bar: 'baz'}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find: {id: 5}}), context)); - assert(rule.isMatch(make_request('query', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('query', 'fake', { }), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {bar: 'baz'}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: {id: 5}}), context)); + assert(rule.isMatch(makeRequest('query', 'test', { }), context)); }); it('collection watch', () => { const rule = new Rule({template: 'collection("test").watch()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('subscribe', 'fake', { }), context)); - assert(!rule.isMatch(make_request('subscribe', 'test', {bar: 'baz'}), context)); - assert(!rule.isMatch(make_request('subscribe', 'test', {find: {id: 5}}), context)); - assert(rule.isMatch(make_request('subscribe', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('subscribe', 'fake', { }), context)); + assert(!rule.isMatch(makeRequest('subscribe', 'test', {bar: 'baz'}), context)); + assert(!rule.isMatch(makeRequest('subscribe', 'test', {find: {id: 5}}), context)); + assert(rule.isMatch(makeRequest('subscribe', 'test', { }), context)); }); for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { it(`collection ${type}`, () => { const rule = new Rule({template: `collection("test").${type}(any())`}); assert(rule.isValid()); - assert(!rule.isMatch(make_request(type, 'test', { }), context)); - assert(!rule.isMatch(make_request(type, 'test', {data: { }}), context)); - assert(!rule.isMatch(make_request(type, 'test', {data: []}), context)); - assert(!rule.isMatch(make_request(type, 'fake', {data: [{ }]}), context)); - assert(!rule.isMatch(make_request(type, 'test', {data: [{ }], fake: 6}), context)); - assert(rule.isMatch(make_request(type, 'test', {data: [{ }]}), context)); + assert(!rule.isMatch(makeRequest(type, 'test', { }), context)); + assert(!rule.isMatch(makeRequest(type, 'test', {data: { }}), context)); + assert(!rule.isMatch(makeRequest(type, 'test', {data: []}), context)); + assert(!rule.isMatch(makeRequest(type, 'fake', {data: [{ }]}), context)); + assert(!rule.isMatch(makeRequest(type, 'test', {data: [{ }], fake: 6}), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: [{ }]}), context)); }); it(`collection ${type} batch`, () => { const rule = new Rule({template: `collection("test").${type}(anyArray(any()))`}); assert(rule.isValid()); - assert(!rule.isMatch(make_request(type, 'test', { }), context)); - assert(!rule.isMatch(make_request(type, 'test', {data: { }}), context)); - assert(!rule.isMatch(make_request(type, 'test', {data: [{ }], fake: 6}), context)); - assert(!rule.isMatch(make_request(type, 'fake', {data: [{ }]}), context)); - assert(rule.isMatch(make_request(type, 'test', {data: []}), context)); - assert(rule.isMatch(make_request(type, 'test', {data: [{ }]}), context)); - assert(rule.isMatch(make_request(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); + assert(!rule.isMatch(makeRequest(type, 'test', { }), context)); + assert(!rule.isMatch(makeRequest(type, 'test', {data: { }}), context)); + assert(!rule.isMatch(makeRequest(type, 'test', {data: [{ }], fake: 6}), context)); + assert(!rule.isMatch(makeRequest(type, 'fake', {data: [{ }]}), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: []}), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: [{ }]}), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: [{ }, {bar: 'baz'}]}), context)); }); } it('any write', () => { const rule = new Rule({template: 'collection("test").anyWrite()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('fake', 'test', { }), context)); - assert(!rule.isMatch(make_request('query', 'test', { }), context)); - assert(!rule.isMatch(make_request('store', null, { }), context)); + assert(!rule.isMatch(makeRequest('fake', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('query', 'test', { }), context)); + assert(!rule.isMatch(makeRequest('store', null, { }), context)); for (const type of ['store', 'update', 'insert', 'upsert', 'replace', 'remove']) { - assert(!rule.isMatch(make_request(type, 'fake', { }), context)); - assert(rule.isMatch(make_request(type, 'test', {data: []}), context)); - assert(rule.isMatch(make_request(type, 'test', {data: [{ }]}), context)); - assert(rule.isMatch(make_request(type, 'test', {data: [], bar: 'baz'}), context)); + assert(!rule.isMatch(makeRequest(type, 'fake', { }), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: []}), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: [{ }]}), context)); + assert(rule.isMatch(makeRequest(type, 'test', {data: [], bar: 'baz'}), context)); } }); it('userId in find', () => { const rule = new Rule({template: 'collection("test").find({ owner: userId() }).fetch()'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'test', {find: { }}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find: true}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find: []}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find: {bar: 'baz'}}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find: {owner: (context.id + 1)}}), context)); - assert(!rule.isMatch(make_request('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); - assert(rule.isMatch(make_request('query', 'test', {find: {owner: context.id}}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: { }}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: true}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: []}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: {bar: 'baz'}}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: {owner: (context.id + 1)}}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: {owner: context.id, bar: 'baz'}}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: {owner: context.id}}), context)); }); it('adds readAny() implicitly', () => { { const rule = new Rule({template: 'collection("test")'}); assert(rule.isValid()); - assert(rule.isMatch(make_request('query', 'test', {find: { }}), context)); - assert(rule.isMatch(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: { }}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: {bar: 'baz'}}), context)); } { const rule = new Rule({template: 'collection("test").find({bar: any()})'}); assert(rule.isValid()); - assert(!rule.isMatch(make_request('query', 'test', {find: { }}), context)); - assert(rule.isMatch(make_request('query', 'test', {find: {bar: 'baz'}}), context)); + assert(!rule.isMatch(makeRequest('query', 'test', {find: { }}), context)); + assert(rule.isMatch(makeRequest('query', 'test', {find: {bar: 'baz'}}), context)); } }); diff --git a/plugins/permissions/src/test/validator.js b/plugins/permissions/src/test/validator.js index d0e329d64..2b0a48e37 100644 --- a/plugins/permissions/src/test/validator.js +++ b/plugins/permissions/src/test/validator.js @@ -1,3 +1,20 @@ +'use strict'; + +const Validator = require('../validator'); + +const assert = require('assert'); + +const permittedValidator = '(userRow) => { return true; }'; + +const forbiddenValidator = '(userRow) => { return false; }'; + +const userPermittedValidator = ` +(userRow, a, b) => { + const value = (a && a.id) || (b && b.id); + return userRow.id === (value % 10); +} +`; + // RSI: link this up describe('Validator', () => { it('unparseable', () => { @@ -6,27 +23,27 @@ describe('Validator', () => { it('broken', () => { const validator = new Validator('() => foo'); - assert.throws(() => validator.isValid(), /Validation error/); + assert.throws(() => validator.isValid(), /foo is not defined/); }); it('permitted', () => { - const validator = new Validator(permitted_validator); - assert(validator.isValid({ id: 3 })); - assert(validator.isValid({ id: 3 }, { id: 0 })); - assert(validator.isValid({ id: 3 }, { id: 0 }, { id: 1 })); + const validator = new Validator(permittedValidator); + assert(validator.isValid({id: 3})); + assert(validator.isValid({id: 3}, {id: 0})); + assert(validator.isValid({id: 3}, {id: 0}, {id: 1})); }); it('user permitted', () => { - const validator = new Validator(user_permitted_validator); - assert(validator.isValid({ id: 3 }, { id: 3 })); - assert(validator.isValid({ id: 3 }, { id: 13 })); - assert(!validator.isValid({ id: 3 }, { id: 4 })); + const validator = new Validator(userPermittedValidator); + assert(validator.isValid({id: 3}, {id: 3})); + assert(validator.isValid({id: 3}, {id: 13})); + assert(!validator.isValid({id: 3}, {id: 4})); }); it('forbidden', () => { - const validator = new Validator(forbidden_validator); - assert(!validator.isValid({ id: 3 })); - assert(!validator.isValid({ id: 3 }, { id: 3 })); - assert(!validator.isValid({ id: 3 }, { id: 0 }, { id: 1 })); + const validator = new Validator(forbiddenValidator); + assert(!validator.isValid({id: 3})); + assert(!validator.isValid({id: 3}, {id: 3})); + assert(!validator.isValid({id: 3}, {id: 0}, {id: 1})); }); }); diff --git a/plugins/permissions/src/validator.js b/plugins/permissions/src/validator.js index 159a57d99..b81ebdd8b 100644 --- a/plugins/permissions/src/validator.js +++ b/plugins/permissions/src/validator.js @@ -8,15 +8,15 @@ const {remakeError} = require('@horizon/plugin-utils'); class Validator { constructor(str) { try { - this._fn = vm.runInNewContext(str, { }); + this.fn = vm.runInNewContext(str, { }); } catch (err) { throw remakeError(err); } - assert(typeof this._fn === 'function'); + assert(typeof this.fn === 'function'); } isValid(...args) { - return this._fn(...args); + return this.fn(...args); } } diff --git a/plugins/remove/src/remove.js b/plugins/remove/src/remove.js index fc6ec0109..e4791d677 100644 --- a/plugins/remove/src/remove.js +++ b/plugins/remove/src/remove.js @@ -2,7 +2,7 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); -const hz_v = writes.versionField; +const hzv = writes.versionField; function remove(context) { return (request, response, next) => { @@ -17,25 +17,25 @@ function remove(context) { throw new Error('No permissions given for insert operation.'); } - writes.retry_loop(request.options.remove, permissions, timeout, + writes.retryLoop(request.options.remove, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows.map((row) => row.id)) .map((id) => collection.table.get(id)) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_required(validator, row, info, null), + writes.validateOldRowRequired(validator, row, info, null), (rows) => // write to database, all valid rows - r.expr(rows).do((row_data) => - row_data.forEach((info) => + r.expr(rows).do((rowData) => + rowData.forEach((info) => collection.table.get(info('id')).replace((row) => r.branch(// The row may have been deleted between the get and now row.eq(null), null, // The row may have been changed between the get and now - r.and(info.hasFields(hz_v), - row(hz_v).default(-1).ne(info(hz_v))), - r.error(writes.invalidated_msg), + r.and(info.hasFields(hzv), + row(hzv).default(-1).ne(info(hzv))), + r.error(writes.invalidatedMsg), // Otherwise, we can safely remove the row null), @@ -44,10 +44,10 @@ function remove(context) { // Pretend like we deleted rows that didn't exist .do((res) => res.merge({changes: - r.range(row_data.count()).map((index) => + r.range(rowData.count()).map((index) => r.branch(res('changes')(index)('old_val').eq(null), res('changes')(index).merge( - {old_val: {id: row_data(index)('id')}}), + {old_val: {id: rowData(index)('id')}}), res('changes')(index))).coerceTo('array'), }))) .run(conn, reqlOptions) diff --git a/plugins/replace/src/replace.js b/plugins/replace/src/replace.js index ac784d501..054afa305 100644 --- a/plugins/replace/src/replace.js +++ b/plugins/replace/src/replace.js @@ -2,7 +2,7 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); -const hz_v = writes.versionField; +const hzv = writes.versionField; function replace(context) { return (request, response, next) => { @@ -17,29 +17,29 @@ function replace(context) { throw new Error('No permissions given for insert operation.'); } - writes.retry_loop(request.options.replace, permissions, timeout, + writes.retryLoop(request.options.replace, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows.map((row) => row.id)) .map((id) => collection.table.get(id)) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_required(validator, row, info, row), + writes.validateOldRowRequired(validator, row, info, row), (rows) => // write to database, all valid rows r.expr(rows) - .forEach((new_row) => - collection.table.get(new_row('id')).replace((old_row) => + .forEach((newRow) => + collection.table.get(newRow('id')).replace((oldRow) => r.branch(// The row may have been deleted between the get and now - old_row.eq(null), - r.error(writes.missing_msg), + oldRow.eq(null), + r.error(writes.missingMsg), // The row may have been changed between the get and now - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), + r.and(newRow.hasFields(hzv), + oldRow(hzv).default(-1).ne(newRow(hzv))), + r.error(writes.invalidatedMsg), // Otherwise, we can safely replace the row - writes.apply_version( - new_row, old_row(hz_v).default(-1).add(1))), + writes.applyVersion( + newRow, oldRow(hzv).default(-1).add(1))), {returnChanges: 'always'})) .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); diff --git a/server/src/auth/slack.js b/plugins/slack/src/slack.js similarity index 100% rename from server/src/auth/slack.js rename to plugins/slack/src/slack.js diff --git a/plugins/store/src/store.js b/plugins/store/src/store.js index bf7414bd6..f4f4ab8c7 100644 --- a/plugins/store/src/store.js +++ b/plugins/store/src/store.js @@ -2,7 +2,7 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); -const hz_v = writes.versionField; +const hzv = writes.versionField; function store(context) { return (request, response, next) => { @@ -17,42 +17,42 @@ function store(context) { throw new Error('No permissions given for insert operation.'); } - writes.retry_loop(request.options.store, permissions, timeout, + writes.retryLoop(request.options.store, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows.map((row) => (row.id === undefined ? null : row.id))) .map((id) => r.branch(id.eq(null), null, collection.table.get(id))) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_optional(validator, row, info, row), + writes.validateOldRowOptional(validator, row, info, row), (rows) => // write to database, all valid rows r.expr(rows) - .forEach((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).replace((old_row) => + .forEach((newRow) => + r.branch(newRow.hasFields('id'), + collection.table.get(newRow('id')).replace((oldRow) => r.branch( - old_row.eq(null), + oldRow.eq(null), r.branch( // Error if we were expecting the row to exist - new_row.hasFields(hz_v), - r.error(writes.invalidated_msg), + newRow.hasFields(hzv), + r.error(writes.invalidatedMsg), // Otherwise, insert the row - writes.apply_version(new_row, 0) + writes.applyVersion(newRow, 0) ), r.branch( // The row may have changed from the expected version - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), + r.and(newRow.hasFields(hzv), + oldRow(hzv).default(-1).ne(newRow(hzv))), + r.error(writes.invalidatedMsg), // Otherwise, we can overwrite the row - writes.apply_version(new_row, - old_row(hz_v).default(-1).add(1)) + writes.applyVersion(newRow, + oldRow(hzv).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row does not have an id, so it will autogenerate - collection.table.insert(writes.apply_version(new_row, 0), + collection.table.insert(writes.applyVersion(newRow, 0), {returnChanges: 'always'}))) .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); diff --git a/server/src/auth/twitch.js b/plugins/twitch/src/twitch.js similarity index 100% rename from server/src/auth/twitch.js rename to plugins/twitch/src/twitch.js diff --git a/server/src/auth/twitter.js b/plugins/twitter/src/twitter.js similarity index 100% rename from server/src/auth/twitter.js rename to plugins/twitter/src/twitter.js diff --git a/plugins/update/src/update.js b/plugins/update/src/update.js index aee3d9a3f..5d40d3966 100644 --- a/plugins/update/src/update.js +++ b/plugins/update/src/update.js @@ -2,7 +2,7 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); -const hz_v = writes.versionField; +const hzv = writes.versionField; function update(context) { return (request, response, next) => { @@ -17,33 +17,33 @@ function update(context) { throw new Error('No permissions given for insert operation.'); } - writes.retry_loop(request.options.update, permissions, timeout, + writes.retryLoop(request.options.update, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows) - .map((new_row) => - collection.table.get(new_row('id')).do((old_row) => - r.branch(old_row.eq(null), + .map((newRow) => + collection.table.get(newRow('id')).do((oldRow) => + r.branch(oldRow.eq(null), null, - [old_row, old_row.merge(new_row)]))) + [oldRow, oldRow.merge(newRow)]))) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_required(validator, row, info[0], info[1]), + writes.validateOldRowRequired(validator, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) - .forEach((new_row) => - collection.table.get(new_row('id')).replace((old_row) => + .forEach((newRow) => + collection.table.get(newRow('id')).replace((oldRow) => r.branch(// The row may have been deleted between the get and now - old_row.eq(null), - r.error(writes.missing_msg), + oldRow.eq(null), + r.error(writes.missingMsg), // The row may have been changed between the get and now - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), + r.and(newRow.hasFields(hzv), + oldRow(hzv).default(-1).ne(newRow(hzv))), + r.error(writes.invalidatedMsg), // Otherwise we can update the row and increment the version - writes.apply_version(old_row.merge(new_row), - old_row(hz_v).default(-1).add(1))), + writes.applyVersion(oldRow.merge(newRow), + oldRow(hzv).default(-1).add(1))), {returnChanges: 'always'})) .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/upsert/src/upsert.js b/plugins/upsert/src/upsert.js index c811ed8e4..6e135972a 100644 --- a/plugins/upsert/src/upsert.js +++ b/plugins/upsert/src/upsert.js @@ -2,7 +2,7 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); -const hz_v = writes.versionField; +const hzv = writes.versionField; function upsert(context) { return (request, response, next) => { @@ -17,48 +17,48 @@ function upsert(context) { throw new Error('No permissions given for insert operation.'); } - writes.retry_loop(request.options.upsert, permissions, timeout, + writes.retryLoop(request.options.upsert, permissions, timeout, (rows) => // pre-validation, all rows r.expr(rows) - .map((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).do((old_row) => - r.branch(old_row.eq(null), - [null, new_row], - [old_row, old_row.merge(new_row)])), - [null, new_row])) + .map((newRow) => + r.branch(newRow.hasFields('id'), + collection.table.get(newRow('id')).do((oldRow) => + r.branch(oldRow.eq(null), + [null, newRow], + [oldRow, oldRow.merge(newRow)])), + [null, newRow])) .run(conn, reqlOptions), (validator, row, info) => - writes.validate_old_row_optional(validator, row, info[0], info[1]), + writes.validateOldRowOptional(validator, row, info[0], info[1]), (rows) => // write to database, all valid rows r.expr(rows) - .forEach((new_row) => - r.branch(new_row.hasFields('id'), - collection.table.get(new_row('id')).replace((old_row) => + .forEach((newRow) => + r.branch(newRow.hasFields('id'), + collection.table.get(newRow('id')).replace((oldRow) => r.branch( - old_row.eq(null), + oldRow.eq(null), r.branch( // Error if we were expecting the row to exist - new_row.hasFields(hz_v), - r.error(writes.invalidated_msg), + newRow.hasFields(hzv), + r.error(writes.invalidatedMsg), // Otherwise, insert the row - writes.apply_version(new_row, 0) + writes.applyVersion(newRow, 0) ), r.branch( // The row may have changed from the expected version - r.and(new_row.hasFields(hz_v), - old_row(hz_v).default(-1).ne(new_row(hz_v))), - r.error(writes.invalidated_msg), + r.and(newRow.hasFields(hzv), + oldRow(hzv).default(-1).ne(newRow(hzv))), + r.error(writes.invalidatedMsg), // Otherwise, we can update the row and version - writes.apply_version(old_row.merge(new_row), - old_row(hz_v).default(-1).add(1)) + writes.applyVersion(oldRow.merge(newRow), + oldRow(hzv).default(-1).add(1)) ) ), {returnChanges: 'always'}), // The new row did not have an id, so it will autogenerate - collection.table.insert(writes.apply_version(new_row, 0), + collection.table.insert(writes.applyVersion(newRow, 0), {returnChanges: 'always'}))) .run(conn, reqlOptions) ).then((msg) => response.end(msg)).catch(next); diff --git a/plugins/watch/src/watch.js b/plugins/watch/src/watch.js index 02f55d6df..36ecca04f 100644 --- a/plugins/watch/src/watch.js +++ b/plugins/watch/src/watch.js @@ -15,10 +15,10 @@ function watch(context) { } else { reads.makeReadReql(req).then((reql) => reql.changes({ - include_initial: true, - include_states: true, - include_types: true, - include_offsets: + includeInitial: true, + includeStates: true, + includeTypes: true, + includeOffsets: req.getParameter('order') !== undefined && req.getParameter('limit') !== undefined, }).run(conn, reqlOptions) diff --git a/server/src/auth.js b/server/src/auth.js index 411b0ae90..b24a6e219 100644 --- a/server/src/auth.js +++ b/server/src/auth.js @@ -1,7 +1,7 @@ 'use strict'; const logger = require('./logger'); -const options_schema = require('./schema/server_options').auth; +const {authSchema} = require('./schema').auth; const Joi = require('joi'); const Promise = require('bluebird'); @@ -9,7 +9,6 @@ const jwt = Promise.promisifyAll(require('jsonwebtoken')); const r = require('rethinkdb'); const url = require('url'); - class JWT { constructor(options) { this.duration = options.duration; @@ -44,19 +43,13 @@ class JWT { class Auth { - constructor(server, user_options) { - const options = Joi.attempt(user_options, options_schema); - + constructor(projectName, rdbConnection, options) { + // RSI: don't expose token_secret to plugins + this.options = Joi.attempt(options, authSchema); + this.rdbConnection = rdbConnection; this._jwt = new JWT(options); - - this._success_redirect = url.parse(options.success_redirect); - this._failure_redirect = url.parse(options.failure_redirect); - this._create_new_users = options.create_new_users; - this._new_user_group = options.new_user_group; - this._allow_anonymous = options.allow_anonymous; - this._allow_unauthenticated = options.allow_unauthenticated; - - this._parent = server; + this.successUrl = url.parse(this.options.success_redirect); + this.failureUrl = url.parse(this.options.failure_redirect); } handshake(request) { @@ -64,12 +57,12 @@ class Auth { case 'token': return this._jwt.verify(request.token); case 'unauthenticated': - if (!this._allow_unauthenticated) { + if (!this.options.allow_unauthenticated) { throw new Error('Unauthenticated connections are not allowed.'); } return this._jwt.verify(this._jwt.sign({id: null, provider: request.method}).token); case 'anonymous': - if (!this._allow_anonymous) { + if (!this.options.allow_anonymous) { throw new Error('Anonymous connections are not allowed.'); } return this.generate(request.method, r.uuid()); @@ -79,7 +72,7 @@ class Auth { } // Can't use objects in primary keys, so convert those to JSON in the db (deterministically) - auth_key(provider, info) { + authKey(provider, info) { if (info === null || Array.isArray(info) || typeof info !== 'object') { return [provider, info]; } else { @@ -87,18 +80,18 @@ class Auth { } } - new_user_row(id) { + newUserRow(id) { return { id, - groups: ['default', this._new_user_group], + groups: ['default', this.options.new_user_group], }; } // TODO: maybe we should write something into the user data to track open sessions/tokens generate(provider, info) { return Promise.resolve().then(() => { - const conn = this._parent.rdbConnection.connection(); - const key = this.auth_key(provider, info); + const conn = this.rdbConnection.connection(); + const key = this.authKey(provider, info); const db = r.db(this._parent.options.project_name); const insert = (table, row) => @@ -110,9 +103,9 @@ class Auth { .get(db.table('hz_users_auth').get(key)('user_id')) .default(r.error('User not found and new user creation is disabled.')); - if (this._create_new_users) { + if (this.options.create_new_users) { query = insert('hz_users_auth', {id: key, user_id: r.uuid()}) - .do((auth_user) => insert('users', this.new_user_row(auth_user('user_id')))); + .do((authUser) => insert('users', this.newUserRow(authUser('user_id')))); } return query.run(conn).catch((err) => { diff --git a/server/src/client.js b/server/src/client.js index a6953964d..56a438cf1 100644 --- a/server/src/client.js +++ b/server/src/client.js @@ -22,31 +22,31 @@ class ClientConnection { this.responses = new Map(); this.socket.on('close', (code, msg) => - this.handle_websocket_close(code, msg)); + this.handleWebsocketClose(code, msg)); this.socket.on('error', (error) => - this.handle_websocket_error(error)); + this.handleWebsocketError(error)); // The first message should always be the handshake this.socket.once('message', (data) => - this.error_wrap_socket(() => this.handle_handshake(data))); + this.errorWrapSocket(() => this.handleHandshake(data))); } context() { return this._context; } - handle_websocket_close() { + handleWebsocketClose() { logger.debug('ClientConnection closed.'); this.responses.forEach((response) => response.end()); this.responses.clear(); } - handle_websocket_error(code, msg) { + handleWebsocketError(code, msg) { logger.error(`Received error from client: ${msg} (${code})`); } - error_wrap_socket(cb) { + errorWrapSocket(cb) { try { cb(); } catch (err) { @@ -59,7 +59,7 @@ class ClientConnection { } } - parse_request(data, schema) { + parseRequest(data, schema) { let request; try { request = JSON.parse(data); @@ -75,20 +75,20 @@ class ClientConnection { return Joi.attempt(request, schema); } catch (err) { const detail = err.details[0]; - const err_str = `Request validation error at "${detail.path}": ${detail.message}`; + const errStr = `Request validation error at "${detail.path}": ${detail.message}`; const reqId = request.request_id === undefined ? null : request.request_id; if (request.request_id === undefined) { // This is pretty much an unrecoverable protocol error, so close the connection - this.close({reqId, error: `Protocol error: ${err_str}`, error_code: 0}); + this.close({reqId, error: `Protocol error: ${errStr}`, error_code: 0}); } else { - this.send_error(reqId, new Error(err_str)); + this.sendError(reqId, new Error(errStr)); } } } - handle_handshake(data) { - const request = this.parse_request(data, schemas.handshake); + handleHandshake(data) { + const request = this.parseRequest(data, schemas.handshake); logger.debug(`Received handshake: ${JSON.stringify(request)}`); if (request === undefined) { @@ -98,13 +98,13 @@ class ClientConnection { const reqId = request.request_id; this.auth.handshake(request).then((res) => { this._context.user = res.payload; - this.send_message(reqId, { + this.sendMessage(reqId, { token: res.token, id: res.payload.id, provider: res.payload.provider, }); this.socket.on('message', (msg) => - this.error_wrap_socket(() => this.handle_request(msg))); + this.errorWrapSocket(() => this.handleRequest(msg))); this.clientEvents.emit('auth', this._context); }).catch((err) => { logger.debug(`Error during client handshake: ${err.stack}`); @@ -112,49 +112,49 @@ class ClientConnection { }); } - handle_request(data) { + handleRequest(data) { logger.debug(`Received request from client: ${data}`); - const raw_request = this.parse_request(data, schemas.request); + const rawRequest = this.parseRequest(data, schemas.request); - if (raw_request === undefined) { + if (rawRequest === undefined) { return; } - const reqId = raw_request.request_id; - if (raw_request.type === 'keepalive') { - return this.send_message(reqId, {state: 'complete'}); - } else if (raw_request.type === 'end_subscription') { + const reqId = rawRequest.request_id; + if (rawRequest.type === 'keepalive') { + return this.sendMessage(reqId, {state: 'complete'}); + } else if (rawRequest.type === 'end_subscription') { // there is no response for end_subscription - return this.remove_response(reqId); + return this.removeResponse(reqId); } else if (this.responses.get(reqId)) { - return this.close({ error: `Received duplicate request_id: ${reqId}` }); + return this.close({error: `Received duplicate request_id: ${reqId}`}); } - Object.freeze(raw_request.options); - raw_request.clientCtx = this._context; - raw_request._parameters = {}; + Object.freeze(rawRequest.options); + rawRequest.clientCtx = this._context; + rawRequest._parameters = {}; - const response = new Response((obj) => this.send_message(reqId, obj)); + const response = new Response((obj) => this.sendMessage(reqId, obj)); this.responses.set(reqId, response); response.complete.then(() => this.remove_request(reqId)); - this.requestHandlerCb(raw_request, response, (err) => + this.requestHandlerCb(rawRequest, response, (err) => response.end(err || new Error('Request ran past the end of the ' + 'request handler stack.'))); } - remove_response(request_id) { + removeResponse(request_id) { const response = this.responses.get(request_id); this.responses.delete(request_id); response.end(); } - is_open() { + isOpen() { return this.socket.readyState === websocket.OPEN; } close(info) { - if (this.is_open()) { + if (this.isOpen()) { const reason = (info.error && info.error.substr(0, 64)) || 'Unspecified reason.'; logger.debug('Closing ClientConnection with reason: ' + @@ -167,22 +167,22 @@ class ClientConnection { } } - send_message(reqId, data) { + sendMessage(reqId, data) { // Ignore responses for disconnected clients - if (this.is_open()) { + if (this.isOpen()) { data.request_id = reqId; logger.debug(`Sending response: ${JSON.stringify(data)}`); this.socket.send(JSON.stringify(data)); } } - send_error(reqId, err, code) { + sendError(reqId, err, code) { logger.debug( `Sending error result for request ${reqId}:\n${err.stack}`); const error = err instanceof Error ? err.message : err; const error_code = code === undefined ? -1 : code; - this.send_message(reqId, {error, error_code}); + this.sendMessage(reqId, {error, error_code}); } } diff --git a/server/src/error.js b/server/src/error.js deleted file mode 100644 index ec81ff3c9..000000000 --- a/server/src/error.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -const assert = require('assert'); - -class IndexMissing extends Error { - constructor(collection, fields) { - super(`Collection "${collection.name}" has no index matching ${JSON.stringify(fields)}.`); - this.collection = collection; - this.fields = fields; - } -} - -class CollectionMissing extends Error { - constructor(name) { - super(`Collection "${name}" does not exist.`); - this.name = name; - } -} - -class IndexNotReady extends Error { - constructor(collection, index) { - super(`Index on collection "${collection.name}" is not ready: ${JSON.stringify(index.fields)}.`); - this.collection = collection; - this.index = index; - } -} - -class CollectionNotReady extends Error { - constructor(collection) { - super(`Collection "${collection.name}" is not ready.`); - this.collection = collection; - } -} - -module.exports = { - IndexMissing, - IndexNotReady, - CollectionMissing, - CollectionNotReady, -}; diff --git a/server/src/index.js b/server/src/index.js index 5a493e4d3..cdf10725b 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -38,26 +38,26 @@ module.exports.clientSource = function() { clientSource = fs.readFileSync(clientSourcePath); } return clientSource; -} +}; module.exports.clientSourceMap = function() { if (!clientSourceMap) { clientSourceMap = fs.readFileSync(`${clientSourcePath}.map`); } return clientSourceMap; -} +}; -modules.exports.clientSourceCore = function() { +module.exports.clientSourceCore = function() { if (!clientSourceCore) { clientSourceCore = fs.readFileSync(clientSourceCorePath); } return clientSourceCore; -} +}; -modules.exports.clientSourceCoreMap = function() { +module.exports.clientSourceCoreMap = function() { if (!clientSourceCoreMap) { clientSourceCoreMap = fs.readFileSync(`${clientSourceCorePath}.map`); } return clientSourceCoreMap; -} +}; diff --git a/server/src/middleware_pattern_factory_factory_pattern_strategy_factory.js b/server/src/middleware_pattern_factory_factory_pattern_strategy_factory.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/src/reliable.js b/server/src/reliable.js index 0d077dc33..b4cb79fb4 100644 --- a/server/src/reliable.js +++ b/server/src/reliable.js @@ -1,11 +1,12 @@ 'use strict'; -import * as r from 'rethinkdb'; const logger = require('./logger'); +const r = require('rethinkdb'); + const subs = Symbol('subs'); -export class Reliable { +class Reliable { constructor(initialCbs) { this[subs] = new Map(); this.ready = false; @@ -77,7 +78,7 @@ export class Reliable { } } -export class ReliableUnion extends Reliable { +class ReliableUnion extends Reliable { constructor(reqs, cbs) { super(cbs); this.reqs = reqs; @@ -117,7 +118,7 @@ export class ReliableUnion extends Reliable { } } -export class ReliableConn extends Reliable { +class ReliableConn extends Reliable { constructor(connOpts) { super(); this.connOpts = connOpts; @@ -171,16 +172,16 @@ export class ReliableConn extends Reliable { } } -export class ReliableChangefeed extends Reliable { +class ReliableChangefeed extends Reliable { constructor(reql, reliableConn, cbs) { super(cbs); this.reql = reql; this.reliableConn = reliableConn; - this.make_subscription(); + this._makeSubscription(); } - make_subscription() { + _makeSubscription() { if (this.closed) { return; } this.subscription = this.reliableConn.subscribe({ onReady: (conn) => { @@ -212,7 +213,7 @@ export class ReliableChangefeed extends Reliable { if (this.subscription) { this.subscription.close(); this.subscription = null; - setTimeout(() => this.make_subscription(), 1000); + setTimeout(() => this._makeSubscription(), 1000); } }); }, @@ -231,3 +232,10 @@ export class ReliableChangefeed extends Reliable { return retProm; } } + +module.exports = { + Reliable, + ReliableUnion, + ReliableConn, + ReliableChangefeed, +}; diff --git a/server/src/request.js b/server/src/request.js index f6c2ff1f7..93ec77ba3 100644 --- a/server/src/request.js +++ b/server/src/request.js @@ -4,7 +4,7 @@ const assert = require('assert'); class Request { constructor(request, currentMethod) { - this.request_id = request.request_id; + this.requestId = request.request_id; this.options = request.options; this.clientCtx = request.clientCtx; this._parameters = request._parameters; @@ -18,7 +18,7 @@ class Request { setParameter(value) { assert(this._currentMethod); - return this._parameters[this._currentMethod] = value; + this._parameters[this._currentMethod] = value; } } diff --git a/server/src/schema/server_options.js b/server/src/schema.js similarity index 55% rename from server/src/schema/server_options.js rename to server/src/schema.js index 7aaea84ef..1396fc773 100644 --- a/server/src/schema/server_options.js +++ b/server/src/schema.js @@ -2,7 +2,8 @@ const Joi = require('joi'); -const options = Joi.object({ +// Options for Server object construction +const server = Joi.object({ project_name: Joi.string().default('horizon'), rdb_host: Joi.string().hostname().default('localhost'), rdb_port: Joi.number().greater(0).less(65536).default(28015), @@ -16,6 +17,7 @@ const options = Joi.object({ rdb_timeout: Joi.number().allow(null), }).unknown(false); +// Options for Auth object construction const auth = Joi.object({ success_redirect: Joi.string().default('/'), failure_redirect: Joi.string().default('/'), @@ -30,10 +32,28 @@ const auth = Joi.object({ allow_unauthenticated: Joi.boolean().default(false), }).unknown(false); +// Options for server.addMethod() const method = Joi.object({ type: Joi.valid('middleware', 'option', 'prereq', 'terminal').required(), handler: Joi.func().minArity(2).maxArity(3).required(), requires: Joi.array().single().items(Joi.string()).default([]), }).unknown(false); -module.exports = {options, auth, method}; +// The Horizon protocol handshake message +const handshake = Joi.object().keys({ + request_id: Joi.number().required(), + method: Joi.only('token', 'anonymous', 'unauthenticated').required(), + token: Joi.string().required() + .when('method', {is: Joi.not('token').required(), then: Joi.forbidden()}), +}).unknown(false); + +// Every Horizon protocol request (following the handshake) +const request = Joi.object({ + request_id: Joi.number().required(), + type: Joi.only('end_subscription', 'keepalive').optional(), + options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required() + .when('type', {is: Joi.string().only('end_subscription', 'keepalive').required(), + then: Joi.forbidden()}), +}).unknown(false); + +module.exports = {server, auth, method, handshake, request}; diff --git a/server/src/schema/horizon_protocol.js b/server/src/schema/horizon_protocol.js deleted file mode 100644 index 4eb1a7489..000000000 --- a/server/src/schema/horizon_protocol.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -const Joi = require('joi'); - -const handshake = Joi.object().keys({ - request_id: Joi.number().required(), - method: Joi.only('token', 'anonymous', 'unauthenticated').required(), - token: Joi.string().required() - .when('method', {is: Joi.not('token').required(), then: Joi.forbidden()}), -}).unknown(false); - -const request = Joi.object({ - request_id: Joi.number().required(), - type: Joi.only('end_subscription', 'keepalive').optional(), - options: Joi.object().pattern(/.*/, Joi.array()).unknown(true).required() - .when('type', {is: Joi.string().only('end_subscription', 'keepalive').required(), - then: Joi.forbidden()}) -}).unknown(false); - -module.exports = { - handshake, - request, -}; diff --git a/server/src/server.js b/server/src/server.js index 6be353ce4..c2eedaabf 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -3,15 +3,14 @@ const Auth = require('./auth').Auth; const ClientConnection = require('./client'); const logger = require('./logger'); -const {ReliableConn, ReliableChangefeed} = require('./reliable'); -const schema = require('./schema/server_options'); +const {ReliableConn} = require('./reliable'); +const schema = require('./schema'); const Request = require('./request'); const EventEmitter = require('events'); const Joi = require('joi'); const websocket = require('ws'); -const r = require('rethinkdb'); const Toposort = require('toposort-class'); const protocolName = 'rethinkdb-horizon-v1'; @@ -26,15 +25,12 @@ function handleProtocols(protocols, cb) { } class Server { - constructor(http_servers, user_opts) { - this.options = Joi.attempt(user_opts || { }, schema.options); - this._auth_methods = { }; - this._request_handlers = new Map(); - this._ws_servers = []; - this._close_promise = null; + constructor(httpServers, options) { + this.options = Joi.attempt(options || { }, schema.server); + this._wsServers = []; + this._closePromise = null; this._methods = {}; this._middlewareMethods = new Set(); - this._auth = new Auth(this, this.options.auth); this._clients = new Set(); this.events = new EventEmitter(); @@ -47,7 +43,7 @@ class Server { timeout: this.options.rdb_timeout || null, }); - this._clear_clients_subscription = this.rdbConnection.subscribe({ + this._clearClientsSubscription = this.rdbConnection.subscribe({ onReady: () => { this.events.emit('ready', this); }, @@ -59,6 +55,8 @@ class Server { }, }); + this._auth = new Auth(this.options.project_name, this.rdbConnection, this.options.auth); + this._requestHandler = (req, res, next) => { let terminal = null; const requirements = {}; @@ -123,12 +121,12 @@ class Server { } }; - const ws_options = {handleProtocols, verifyClient, path: this.options.path}; + const wsOptions = {handleProtocols, verifyClient, path: this.options.path}; // RSI: only become ready when the websocket servers and the // connection are both ready. - const add_websocket = (server) => { - const ws_server = new websocket.Server(Object.assign({server}, ws_options)) + const addWebsocket = (server) => { + const wsServer = new websocket.Server(Object.assign({server}, wsOptions)) .on('error', (error) => logger.error(`Websocket server error: ${error}`)) .on('connection', (socket) => { try { @@ -140,7 +138,7 @@ class Server { socket, this._auth, this._requestHandler, - this.events, + this.events ); this._clients.add(client); this.events.emit('connect', client.context()); @@ -156,13 +154,13 @@ class Server { } }); - this._ws_servers.push(ws_server); + this._wsServers.push(wsServer); }; - if (http_servers.forEach === undefined) { - add_websocket(http_servers); + if (httpServers.forEach === undefined) { + addWebsocket(httpServers); } else { - http_servers.forEach((s) => add_websocket(s)); + httpServers.forEach((s) => addWebsocket(s)); } } @@ -170,15 +168,15 @@ class Server { return this._auth; } - addMethod(name, raw_options) { - const options = Joi.attempt(raw_options, schema.method); + addMethod(name, rawOptions) { + const options = Joi.attempt(rawOptions, schema.method); if (this._methods[name]) { throw new Error(`"${name}" is already registered as a method.`); } this._methods[name] = options; if (options.type === 'middleware') { - this._middlewareMethods.add(name); + this._middlewareMethods.add(name); } this._requirementsOrdering = null; @@ -213,13 +211,13 @@ class Server { // TODO: We close clients in `onUnready` above, but don't wait for them to be closed. close() { - if (!this._close_promise) { - this._close_promise = - Promise.all(this._ws_servers.map((s) => new Promise((resolve) => { + if (!this._closePromise) { + this._closePromise = + Promise.all(this._wsServers.map((s) => new Promise((resolve) => { s.close(resolve); }))).then(() => this.rdbConnection.close()); } - return this._close_promise; + return this._closePromise; } } From 203b420991d3baa7867c29a3c4300489ac33fe23 Mon Sep 17 00:00:00 2001 From: Marc Hesse Date: Thu, 15 Sep 2016 17:06:21 -0700 Subject: [PATCH 86/86] checkpoint --- plugin-router/base/.babelrc | 3 - plugin-router/base/package.json | 5 +- plugin-router/express/.babelrc | 3 - plugin-router/express/package.json | 5 +- plugin-router/hapi/.babelrc | 3 - plugin-router/hapi/package.json | 5 +- plugin-router/http/.babelrc | 3 - plugin-router/http/package.json | 5 +- plugin-router/koa/.babelrc | 3 - plugin-router/koa/package.json | 5 +- plugin-utils/.babelrc | 3 - plugin-utils/.eslintrc.js | 17 ++ plugin-utils/package.json | 6 +- plugin-utils/src/auth-utils.js | 199 --------------------- plugin-utils/src/auth.js | 203 ++++++++++++++++++++++ plugin-utils/src/reads.js | 43 +++-- plugin-utils/src/test.js | 15 +- plugin-utils/src/utils.js | 12 +- plugin-utils/src/writes.js | 178 +++++++++---------- plugins/above/.babelrc | 3 - plugins/above/package.json | 5 +- plugins/below/.babelrc | 3 - plugins/below/package.json | 5 +- plugins/collection/.babelrc | 3 - plugins/collection/package.json | 5 +- plugins/defaults/.babelrc | 3 - plugins/defaults/package.json | 5 +- plugins/fetch/.babelrc | 3 - plugins/fetch/package.json | 5 +- plugins/fetch/src/fetch.js | 20 ++- plugins/find/.babelrc | 3 - plugins/find/package.json | 5 +- plugins/findAll/.babelrc | 3 - plugins/findAll/package.json | 5 +- plugins/insert/.babelrc | 3 - plugins/insert/package.json | 5 +- plugins/insert/src/insert.js | 12 +- plugins/limit/.babelrc | 3 - plugins/limit/package.json | 5 +- plugins/order/.babelrc | 3 - plugins/order/package.json | 5 +- plugins/permissions/.babelrc | 3 - plugins/permissions/package.json | 5 +- plugins/permissions/src/template.js | 249 ++++++++++++--------------- plugins/permit-all/.babelrc | 3 - plugins/permit-all/package.json | 5 +- plugins/permit-all/src/permit-all.js | 2 +- plugins/remove/.babelrc | 3 - plugins/remove/package.json | 5 +- plugins/remove/src/remove.js | 12 +- plugins/replace/.babelrc | 3 - plugins/replace/package.json | 5 +- plugins/replace/src/replace.js | 12 +- plugins/store/.babelrc | 3 - plugins/store/package.json | 5 +- plugins/store/src/store.js | 12 +- plugins/timeout/.babelrc | 3 - plugins/timeout/package.json | 5 +- plugins/update/.babelrc | 3 - plugins/update/package.json | 5 +- plugins/update/src/update.js | 12 +- plugins/upsert/.babelrc | 3 - plugins/upsert/package.json | 5 +- plugins/upsert/src/upsert.js | 12 +- plugins/watch/.babelrc | 3 - plugins/watch/package.json | 8 +- plugins/watch/src/watch.js | 56 ++++-- protocol.md | 131 +++++--------- server/.babelrc | 3 - server/package.json | 9 +- server/src/auth.js | 2 +- server/src/client.js | 10 +- server/src/response.js | 41 +++-- test/.babelrc | 3 - test/package.json | 16 +- test/src/test/protocol.js | 35 ++-- test/src/test/schema.js | 2 +- test/src/test/utils.js | 35 +++- test/src/test/write.js | 138 +++++++-------- 79 files changed, 812 insertions(+), 883 deletions(-) delete mode 100644 plugin-router/base/.babelrc delete mode 100644 plugin-router/express/.babelrc delete mode 100644 plugin-router/hapi/.babelrc delete mode 100644 plugin-router/http/.babelrc delete mode 100644 plugin-router/koa/.babelrc delete mode 100644 plugin-utils/.babelrc create mode 100644 plugin-utils/.eslintrc.js delete mode 100644 plugin-utils/src/auth-utils.js create mode 100644 plugin-utils/src/auth.js delete mode 100644 plugins/above/.babelrc delete mode 100644 plugins/below/.babelrc delete mode 100644 plugins/collection/.babelrc delete mode 100644 plugins/defaults/.babelrc delete mode 100644 plugins/fetch/.babelrc delete mode 100644 plugins/find/.babelrc delete mode 100644 plugins/findAll/.babelrc delete mode 100644 plugins/insert/.babelrc delete mode 100644 plugins/limit/.babelrc delete mode 100644 plugins/order/.babelrc delete mode 100644 plugins/permissions/.babelrc delete mode 100644 plugins/permit-all/.babelrc delete mode 100644 plugins/remove/.babelrc delete mode 100644 plugins/replace/.babelrc delete mode 100644 plugins/store/.babelrc delete mode 100644 plugins/timeout/.babelrc delete mode 100644 plugins/update/.babelrc delete mode 100644 plugins/upsert/.babelrc delete mode 100644 plugins/watch/.babelrc delete mode 100644 server/.babelrc delete mode 100644 test/.babelrc diff --git a/plugin-router/base/.babelrc b/plugin-router/base/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugin-router/base/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugin-router/base/package.json b/plugin-router/base/package.json index 436576f37..83846d29d 100644 --- a/plugin-router/base/package.json +++ b/plugin-router/base/package.json @@ -4,9 +4,8 @@ "description": "Base PluginRouter implementation for Horizon.", "main": "src/index.js", "scripts": { - "lint": "eslint src test", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "lint": "eslint src", + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugin-router/express/.babelrc b/plugin-router/express/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugin-router/express/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugin-router/express/package.json b/plugin-router/express/package.json index 9ed3cfc39..4be761e80 100644 --- a/plugin-router/express/package.json +++ b/plugin-router/express/package.json @@ -4,9 +4,8 @@ "description": "Plugin router for Horizon using Express as the backend.", "main": "src/index.js", "scripts": { - "lint": "eslint src test", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "lint": "eslint src", + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugin-router/hapi/.babelrc b/plugin-router/hapi/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugin-router/hapi/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugin-router/hapi/package.json b/plugin-router/hapi/package.json index 3e150f227..7f7a41338 100644 --- a/plugin-router/hapi/package.json +++ b/plugin-router/hapi/package.json @@ -4,9 +4,8 @@ "description": "Plugin router for Horizon using Hapi as the backend.", "main": "src/index.js", "scripts": { - "lint": "eslint src test", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "lint": "eslint src", + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugin-router/http/.babelrc b/plugin-router/http/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugin-router/http/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugin-router/http/package.json b/plugin-router/http/package.json index 99452813a..8914dbb62 100644 --- a/plugin-router/http/package.json +++ b/plugin-router/http/package.json @@ -4,9 +4,8 @@ "description": "Plugin router for Horizon using a basic HTTP backend.", "main": "src/index.js", "scripts": { - "lint": "eslint src test", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "lint": "eslint src", + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugin-router/koa/.babelrc b/plugin-router/koa/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugin-router/koa/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugin-router/koa/package.json b/plugin-router/koa/package.json index 74263f753..9123f43ad 100644 --- a/plugin-router/koa/package.json +++ b/plugin-router/koa/package.json @@ -4,9 +4,8 @@ "description": "Plugin router for Horizon using Koa as the backend.", "main": "src/index.js", "scripts": { - "lint": "eslint src test", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "lint": "eslint src", + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugin-utils/.babelrc b/plugin-utils/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugin-utils/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugin-utils/.eslintrc.js b/plugin-utils/.eslintrc.js new file mode 100644 index 000000000..bc66210fa --- /dev/null +++ b/plugin-utils/.eslintrc.js @@ -0,0 +1,17 @@ +const OFF = 0; +const WARN = 1; +const ERROR = 2; + +module.exports = { + extends: "../.eslintrc.js", + rules: { + "camelcase": [ ERROR ], + "max-len": [ ERROR, 89 ], + "prefer-template": [ OFF ], + }, + env: { + "es6": true, + "node": true, + "mocha": true, + }, +}; diff --git a/plugin-utils/package.json b/plugin-utils/package.json index 9e3465792..9aa39ca88 100644 --- a/plugin-utils/package.json +++ b/plugin-utils/package.json @@ -2,11 +2,10 @@ "name": "@horizon/plugin-utils", "version": "1.0.0", "description": "Utilities for the default Horizon plugins.", - "main": "dist/utils.js", + "main": "src/utils.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", @@ -22,6 +21,7 @@ "node": ">=4.0.0" }, "dependencies": { + "jsonpatch": "^3.0.1" }, "peerDependencies": { "@horizon/plugin-router": "1.x", diff --git a/plugin-utils/src/auth-utils.js b/plugin-utils/src/auth-utils.js deleted file mode 100644 index 0f6711790..000000000 --- a/plugin-utils/src/auth-utils.js +++ /dev/null @@ -1,199 +0,0 @@ -'use strict'; - -const logger = require('../logger'); - -const cookie = require('cookie'); -const crypto = require('crypto'); -const Joi = require('joi'); -const url = require('url'); - -const do_redirect = (res, redirect_url) => { - logger.debug(`Redirecting user to ${redirect_url}`); - res.writeHead(302, {Location: redirect_url}); - res.end(); -}; - -const extend_url_query = (path, query) => { - const path_copy = Object.assign({}, path); - if (path_copy.query === null) { - path_copy.query = query; - } else { - path_copy.query = Object.assign({}, path_copy.query); - path_copy.query = Object.assign({}, path_copy.query, query); - } - return path_copy; -}; - -const run_request = (req, cb) => { - logger.debug(`Initiating request to ${req._headers.host}${req.path}`); - req.once('response', (res) => { - const chunks = []; - res.on('data', (data) => { - chunks.push(data); - }); - res.once('end', () => { - if (res.statusCode !== 200) { - cb(new Error(`Request returned status code: ${res.statusCode} ` + - `(${res.statusMessage}): ${chunks.join('')}`)); - } else { - cb(null, chunks.join('')); - } - }); - }); - req.once('error', (err) => { - cb(err); - }); - req.end(); -}; - -const try_json_parse = (data) => { - try { - return JSON.parse(data); - } catch (err) { - // Do nothing - just return undefined - } -}; - -const nonce_cookie = (name) => `${name}_horizon_nonce`; - -const make_nonce = (cb) => crypto.randomBytes(64, (err, res) => { - if (!err) { - cb(err, res.toString('base64')); - } else { - cb(err, res); - } -}); - -// TODO: this base64 encoding isn't URL-friendly -const nonce_to_state = (nonce) => - crypto.createHash('sha256').update(nonce, 'base64').digest('base64'); - -const set_nonce = (res, name, nonce) => - res.setHeader('set-cookie', - cookie.serialize(nonce_cookie(name), nonce, - {maxAge: 3600, secure: true, httpOnly: true})); - -const clear_nonce = (res, name) => - res.setHeader('set-cookie', - cookie.serialize(nonce_cookie(name), 'invalid', - {maxAge: -1, secure: true, httpOnly: true})); - -const get_nonce = (req, name) => { - const field = nonce_cookie(name); - if (req.headers.cookie) { - const value = cookie.parse(req.headers.cookie); - return value[field]; - } -}; - -const options_schema = Joi.object({ - horizon: Joi.object().required(), - provider: Joi.string().required(), - make_acquire_url: Joi.func().arity(2).required(), // take `state` and `return_url`, return string - make_token_request: Joi.func().arity(2).required(), // take `code` and `return_url`, return request - make_inspect_request: Joi.func().arity(1).required(), // take `access_token`, return request - extract_id: Joi.func().arity(1).required(), // take `user_info`, return value -}).unknown(false); - -// Attaches an endpoint to the horizon server, providing an oauth2 redirect flow -const oauth2 = (raw_options) => { - const options = Joi.attempt(raw_options, options_schema); - - const horizon = options.horizon; - const provider = options.provider; - const make_acquire_url = options.make_acquire_url; - const make_token_request = options.make_token_request; - const make_inspect_request = options.make_inspect_request; - const extract_id = options.extract_id; - - const self_url = (host, path) => - url.format({protocol: 'https', host: host, pathname: path}); - - const make_success_url = (horizon_token) => - url.format(extend_url_query(horizon._auth._success_redirect, {horizon_token})); - - const make_failure_url = (horizon_error) => - url.format(extend_url_query(horizon._auth._failure_redirect, {horizon_error})); - - horizon.add_http_handler(provider, (req, res) => { - const request_url = url.parse(req.url, true); - const return_url = self_url(req.headers.host, request_url.pathname); - const code = request_url.query && request_url.query.code; - const error = request_url.query && request_url.query.error; - - logger.debug(`oauth request: ${JSON.stringify(request_url)}`); - if (error) { - const description = request_url.query.error_description || error; - do_redirect(res, make_failure_url(description)); - } else if (!code) { - // We need to redirect to the API to acquire a token, then come back and try again - // Generate a nonce to track this client session to prevent CSRF attacks - make_nonce((nonce_err, nonce) => { - if (nonce_err) { - logger.error(`Error creating nonce for oauth state: ${nonce_err}`); - res.statusCode = 503; - res.end('error generating nonce'); - } else { - set_nonce(res, horizon._name, nonce); - do_redirect(res, make_acquire_url(nonce_to_state(nonce), return_url)); - } - }); - } else { - // Make sure this is the same client who obtained the code to prevent CSRF attacks - const nonce = get_nonce(req, horizon._name); - const state = request_url.query.state; - - if (!nonce || !state || state !== nonce_to_state(nonce)) { - do_redirect(res, make_failure_url('session expired')); - } else { - // We have the user code, turn it into an access token - run_request(make_token_request(code, return_url), (err1, body) => { - const info = try_json_parse(body); - const access_token = info && info.access_token; - - if (err1) { - logger.error(`Error contacting oauth API: ${err1}`); - res.statusCode = 503; - res.end('oauth provider error'); - } else if (!access_token) { - logger.error(`Bad JSON data from oauth API: ${body}`); - res.statusCode = 500; - res.end('unparseable token response'); - } else { - // We have the user access token, get info on it so we can find the user - run_request(make_inspect_request(access_token), (err2, inner_body) => { - const user_info = try_json_parse(inner_body); - const user_id = user_info && extract_id(user_info); - - if (err2) { - logger.error(`Error contacting oauth API: ${err2}`); - res.statusCode = 503; - res.end('oauth provider error'); - } else if (!user_id) { - logger.error(`Bad JSON data from oauth API: ${inner_body}`); - res.statusCode = 500; - res.end('unparseable inspect response'); - } else { - horizon._auth.generate(provider, user_id).nodeify((err3, jwt) => { - // Clear the nonce just so we aren't polluting clients' cookies - clear_nonce(res, horizon._name); - do_redirect(res, err3 ? - make_failure_url('invalid user') : - make_success_url(jwt.token)); - }); - } - }); - } - }); - } - } - }); -}; - -module.exports = { - oauth2, - do_redirect, run_request, - make_nonce, set_nonce, get_nonce, clear_nonce, nonce_to_state, - extend_url_query, - try_json_parse, -}; diff --git a/plugin-utils/src/auth.js b/plugin-utils/src/auth.js new file mode 100644 index 000000000..98091b6f4 --- /dev/null +++ b/plugin-utils/src/auth.js @@ -0,0 +1,203 @@ +'use strict'; + +const logger = require('../logger'); + +const cookie = require('cookie'); +const crypto = require('crypto'); +const Joi = require('joi'); +const url = require('url'); + +const doRedirect = (res, redirectUrl) => { + logger.debug(`Redirecting user to ${redirectUrl}`); + res.writeHead(302, {Location: redirectUrl}); + res.end(); +}; + +const extendUrlQuery = (path, query) => { + const pathCopy = Object.assign({}, path); + if (pathCopy.query === null) { + pathCopy.query = query; + } else { + pathCopy.query = Object.assign({}, pathCopy.query); + pathCopy.query = Object.assign({}, pathCopy.query, query); + } + return pathCopy; +}; + +const runRequest = (req, cb) => { + logger.debug(`Initiating request to ${req._headers.host}${req.path}`); + req.once('response', (res) => { + const chunks = []; + res.on('data', (data) => { + chunks.push(data); + }); + res.once('end', () => { + if (res.statusCode !== 200) { + cb(new Error(`Request returned status code: ${res.statusCode} ` + + `(${res.statusMessage}): ${chunks.join('')}`)); + } else { + cb(null, chunks.join('')); + } + }); + }); + req.once('error', (err) => { + cb(err); + }); + req.end(); +}; + +const tryJsonParse = (data) => { + try { + return JSON.parse(data); + } catch (err) { + // Do nothing - just return undefined + } +}; + +const nonceCookie = (name) => `${name}_horizon_nonce`; + +const makeNonce = (cb) => crypto.randomBytes(64, (err, res) => { + if (!err) { + cb(err, res.toString('base64')); + } else { + cb(err, res); + } +}); + +// TODO: this base64 encoding isn't URL-friendly +const nonceToState = (nonce) => + crypto.createHash('sha256').update(nonce, 'base64').digest('base64'); + +const setNonce = (res, name, nonce) => + res.setHeader('set-cookie', + cookie.serialize(nonceCookie(name), nonce, + {maxAge: 3600, secure: true, httpOnly: true})); + +const clearNonce = (res, name) => + res.setHeader('set-cookie', + cookie.serialize(nonceCookie(name), 'invalid', + {maxAge: -1, secure: true, httpOnly: true})); + +const getNonce = (req, name) => { + const field = nonceCookie(name); + if (req.headers.cookie) { + const value = cookie.parse(req.headers.cookie); + return value[field]; + } +}; + +const optionsSchema = Joi.object({ + horizon: Joi.object().required(), + provider: Joi.string().required(), + // makeAcquireUrl takes `state` and `returnUrl`, returns a string url + makeAcquireUrl: Joi.func().arity(2).required(), + // makeTokenRequest takes `code` and `returnUrl`, returns an http request + makeTokenRequest: Joi.func().arity(2).required(), + // makeInspectRequest takes `accessToken`, returns an http request + makeInspectRequest: Joi.func().arity(1).required(), + // extractId takes `userInfo`, returns a unique value for the user from the provider + extractId: Joi.func().arity(1).required(), +}).unknown(false); + +// Attaches an endpoint to the horizon server, providing an oauth2 redirect flow +const oauth2 = (rawOptions) => { + const options = Joi.attempt(rawOptions, optionsSchema); + + const horizon = options.horizon; + const provider = options.provider; + const makeAcquireUrl = options.makeAcquireUrl; + const makeTokenRequest = options.makeTokenRequest; + const makeInspectRequest = options.makeInspectRequest; + const extractId = options.extractId; + + const selfUrl = (host, path) => + url.format({protocol: 'https', host: host, pathname: path}); + + const makeSuccessUrl = (horizonToken) => + url.format(extendUrlQuery(horizon._auth._success_redirect, {horizonToken})); + + const makeFailureUrl = (horizonError) => + url.format(extendUrlQuery(horizon._auth._failure_redirect, {horizonError})); + + horizon.add_http_handler(provider, (req, res) => { + const requestUrl = url.parse(req.url, true); + const returnUrl = selfUrl(req.headers.host, requestUrl.pathname); + const code = requestUrl.query && requestUrl.query.code; + const error = requestUrl.query && requestUrl.query.error; + + logger.debug(`oauth request: ${JSON.stringify(requestUrl)}`); + if (error) { + const description = requestUrl.query.error_description || error; + doRedirect(res, makeFailureUrl(description)); + } else if (!code) { + // We need to redirect to the API to acquire a token, then come back and try again + // Generate a nonce to track this client session to prevent CSRF attacks + makeNonce((nonceErr, nonce) => { + if (nonceErr) { + logger.error(`Error creating nonce for oauth state: ${nonceErr}`); + res.statusCode = 503; + res.end('error generating nonce'); + } else { + setNonce(res, horizon._name, nonce); + doRedirect(res, makeAcquireUrl(nonceToState(nonce), returnUrl)); + } + }); + } else { + // Make sure this is the same client who obtained the code to prevent CSRF attacks + const nonce = getNonce(req, horizon._name); + const state = requestUrl.query.state; + + if (!nonce || !state || state !== nonceToState(nonce)) { + doRedirect(res, makeFailureUrl('session expired')); + } else { + // We have the user code, turn it into an access token + runRequest(makeTokenRequest(code, returnUrl), (err1, body) => { + const info = tryJsonParse(body); + const accessToken = info && info.accessToken; + + if (err1) { + logger.error(`Error contacting oauth API: ${err1}`); + res.statusCode = 503; + res.end('oauth provider error'); + } else if (!accessToken) { + logger.error(`Bad JSON data from oauth API: ${body}`); + res.statusCode = 500; + res.end('unparseable token response'); + } else { + // We have the user access token, get info on it so we can find the user + runRequest(makeInspectRequest(accessToken), (err2, innerBody) => { + const userInfo = tryJsonParse(innerBody); + const userId = userInfo && extractId(userInfo); + + if (err2) { + logger.error(`Error contacting oauth API: ${err2}`); + res.statusCode = 503; + res.end('oauth provider error'); + } else if (!userId) { + logger.error(`Bad JSON data from oauth API: ${innerBody}`); + res.statusCode = 500; + res.end('unparseable inspect response'); + } else { + horizon._auth.generate(provider, userId).nodeify((err3, jwt) => { + // Clear the nonce just so we aren't polluting clients' cookies + clearNonce(res, horizon._name); + doRedirect(res, err3 ? + makeFailureUrl('invalid user') : + makeSuccessUrl(jwt.token)); + }); + } + }); + } + }); + } + } + }); +}; + +module.exports = { + oauth2, + doRedirect, runRequest, + makeNonce, setNonce, getNonce, clearNonce, nonceToState, + extendUrlQuery, + tryJsonParse, +}; diff --git a/plugin-utils/src/reads.js b/plugin-utils/src/reads.js index 8fcfb11cd..87872c59f 100644 --- a/plugin-utils/src/reads.js +++ b/plugin-utils/src/reads.js @@ -9,7 +9,7 @@ function objectToFields(obj) { return Object.keys(obj).map((key) => { const value = obj[key]; if (value !== null && typeof value === 'object' && !value.$reql_type$) { - return object_to_fields(value).map((subkeys) => [key].concat(subkeys)); + return objectToFields(value).map((subkeys) => [key].concat(subkeys)); } else { return [key]; } @@ -48,14 +48,15 @@ function isSameField(a, b) { return true; } +// RSI: not needed? // Returns true if the expected field is in the array of fields -function hasField(fields, expected) { - for (let i = 0; i < fields.length; ++i) { - if (isSameField(fields[i], expected)) { - return true; - } - } -} +// function hasField(fields, expected) { +// for (let i = 0; i < fields.length; ++i) { +// if (isSameField(fields[i], expected)) { +// return true; +// } +// } +// } function makeFindReql(collection, find) { return collection.getMatchingIndex(objectToFields(find), []).then((index) => { @@ -110,9 +111,7 @@ function makeFindAllReql(collection, findAll, fixedFields, above, below, descend .orderBy({index: descending ? r.desc(index.name) : index.name}) .between(leftValue || r.minval, rightValue || r.maxval, optargs); }); - })).then((subqueries) => { - return r.union(...subqueries); - }); + })).then((subqueries) => r.union(...subqueries)); } function makeTableScanReql(collection, fixedFields, above, below, descending) { @@ -122,12 +121,14 @@ function makeTableScanReql(collection, fixedFields, above, below, descending) { if (above) { const defaultLeftBound = above.bound === 'closed' ? r.minval : r.maxval; - leftValue = index.fields.map((field) => getIndexValue(field, {}, above.value, defaultLeftBound)); + leftValue = index.fields.map((field) => + getIndexValue(field, {}, above.value, defaultLeftBound)); optargs.leftBound = above.bound; } if (below) { const defaultRightBound = below.bound === 'closed' ? r.maxval : r.minval; - rightValue = index.fields.map((field) => getIndexValue(field, {}, below.value, defaultRightBound)); + rightValue = index.fields.map((field) => + getIndexValue(field, {}, below.value, defaultRightBound)); optargs.rightBound = below.bound; } @@ -136,7 +137,8 @@ function makeTableScanReql(collection, fixedFields, above, below, descending) { if (rightValue) { rightValue = rightValue[0]; } } - let reql = collection.table.orderBy({index: descending ? r.desc(index.name) : index.name}); + const reqlIndex = descending ? r.desc(index.name) : index.name; + let reql = collection.table.orderBy({index: reqlIndex}); if (leftValue || rightValue) { reql = reql.between(leftValue || r.minval, rightValue || r.maxval, optargs); } @@ -169,7 +171,8 @@ function makeReadReql(req) { if (above) { if (order) { if (!isSameField(above.field, orderFields[0])) { - throw new Error('"above" must be on the same field as the first in "order".'); + throw new Error('"above" must be on the same field ' + + 'as the first in "order".'); } } else { orderFields.push(above.field); @@ -187,16 +190,18 @@ function makeReadReql(req) { } } - let reql_promise; + let reqlPromise; if (findAll) { - reql_promise = makeFindAllReql(collection, findAll, orderFields, above, below, descending); + reqlPromise = makeFindAllReql(collection, findAll, orderFields, + above, below, descending); } else { - reql_promise = makeTableScanReql(collection, orderFields, above, below, descending); + reqlPromise = makeTableScanReql(collection, orderFields, + above, below, descending); } const limit = req.getParameter('limit'); return limit === undefined ? - reql_promise : reql_promise.then((reql) => reql.limit(limit)); + reqlPromise : reqlPromise.then((reql) => reql.limit(limit)); } }); } diff --git a/plugin-utils/src/test.js b/plugin-utils/src/test.js index b3a4bab55..ac162eacf 100644 --- a/plugin-utils/src/test.js +++ b/plugin-utils/src/test.js @@ -1,25 +1,24 @@ 'use strict'; -const Response = require('@horizon/server/dist/response'); +const Response = require('@horizon/server/src/response'); +const jsonpatch = require('jsonpatch'); // Mock Response object for use by tests class MockResponse extends Response { constructor() { super((obj) => { - if (obj.data) { - for (const item of obj.data) { - this._data.push(item); - } + if (obj.patch) { + jsonpatch.apply_patch(this._value, obj.patch); } this._messages.push(obj); }); - this._data = []; + this._value = {}; this._messages = []; - this.data = this.complete.then(() => this._data); + this.value = this.complete.then(() => this._value); this.messages = this.complete.then(() => this._messages); } -}; +} module.exports = {MockResponse}; diff --git a/plugin-utils/src/utils.js b/plugin-utils/src/utils.js index 52d8debd7..8f015f543 100644 --- a/plugin-utils/src/utils.js +++ b/plugin-utils/src/utils.js @@ -16,9 +16,9 @@ function versionCompare(actual, minimum) { } // Check that RethinkDB matches version requirements -function rethinkdbVersionCheck(version_string) { - const rethinkdb_version_regex = /^rethinkdb (\d+)\.(\d+)\.(\d+)/i; - const matches = rethinkdb_version_regex.exec(version_string); +function rethinkdbVersionCheck(versionString) { + const rethinkdbVersionRegex = /^rethinkdb (\d+)\.(\d+)\.(\d+)/i; + const matches = rethinkdbVersionRegex.exec(versionString); if (matches) { // Convert strings to ints and remove first match @@ -37,9 +37,9 @@ function rethinkdbVersionCheck(version_string) { // Used when evaluating things in a different VM context - the errors // thrown from there will not evaluate as `instanceof Error`, so we recreate them. function remakeError(err) { - const new_err = new Error(err.message || 'Unknown error when evaluating template.'); - new_err.stack = err.stack || new_err.stack; - throw new_err; + const newErr = new Error(err.message || 'Unknown error when evaluating template.'); + newErr.stack = err.stack || newErr.stack; + throw newErr; } function isObject(x) { diff --git a/plugin-utils/src/writes.js b/plugin-utils/src/writes.js index a76c08ffe..774a74c8b 100644 --- a/plugin-utils/src/writes.js +++ b/plugin-utils/src/writes.js @@ -4,62 +4,62 @@ const assert = require('assert'); const {r} = require('@horizon/server'); -const hz_v = '$hz_v$'; +const hzv = '$hz_v$'; // Common functionality used by write requests -const invalidated_msg = 'Write invalidated by another request, try again.'; -const missing_msg = 'The document was missing.'; -const timeout_msg = 'Operation timed out.'; -const unauthorized_msg = 'Operation not permitted.'; +const invalidatedMsg = 'Write invalidated by another request, try again.'; +const missingMsg = 'The document was missing.'; +const timeoutMsg = 'Operation timed out.'; +const unauthorizedMsg = 'Operation not permitted.'; -function apply_version(row, new_version) { - return row.merge(r.object(hz_v, new_version)); +function applyVersion(row, newVersion) { + return row.merge(r.object(hzv, newVersion)); } -function make_write_response(data) { +function makeWriteResponse(data) { data.forEach((item, index) => { if (item instanceof Error) { data[index] = {error: item.message}; } }); - return data; + return {op: 'replace', path: '', value: {type: 'value', synced: true, val: data}}; } // This function returns a Promise that resolves to an array of responses - -// one for each row in `original_rows`, or rejects with an appropriate error. +// one for each row in `originalRows`, or rejects with an appropriate error. // deadline -> a Date object for when to give up retrying, // or a falsey value for no timeout -// pre_validate -> function (rows): +// preValidate -> function (rows): // rows: all pending rows // return: an array or the promise of an array of info for those rows // (which will be passed to the validate callback) -// validate_row -> function (row, info): +// validateRow -> function (validator, row, info): // validator: The function to validate with // row: The row from the original query -// info: The info returned by the pre_validate step for this row +// info: The info returned by the preValidate step for this row // return: nothing if successful or an error to be put as the response for this row -// do_write -> function (rows): +// doWrite -> function (rows): // rows: all pending rows // return: a (promise of a) ReQL write result object -function retry_loop(original_rows, - permissions, - deadline, - pre_validate, - validate_row, - do_write) { - let first_attempt = true; - const iterate = (row_data_arg, response_data) => { - let row_data = row_data_arg; - if (row_data.length === 0) { - return response_data; - } else if (!first_attempt && deadline) { +function retryLoop(originalRows, + permissions, + deadline, + preValidate, + validateRow, + doWrite) { + let firstAttempt = true; + const iterate = (rowDataArg, responseData) => { + let rowData = rowDataArg; + if (rowData.length === 0) { + return responseData; + } else if (!firstAttempt && deadline) { if (Date.now() > deadline.getTime()) { - response_data.forEach((data, index) => { + responseData.forEach((data, index) => { if (data === null) { - response_data[index] = new Error(timeout_msg); + responseData[index] = new Error(timeoutMsg); } }); - return response_data; + return responseData; } } @@ -68,124 +68,124 @@ function retry_loop(original_rows, // so we have to restore it to the original value. // This is done because validation only approves moving from one specific // version of the row to another. Even if the original request did not choose - // the version, we are locked in to the version fetched from the pre_validate + // the version, we are locked in to the version fetched from the preValidate // callback until the next iteration. If the version has changed in the meantime, // it is an invalidated error which may be retried until we hit the deadline. - row_data.forEach((data) => { + rowData.forEach((data) => { if (data.version === undefined) { - delete data.row[hz_v]; + delete data.row[hzv]; } else { - data.row[hz_v] = data.version; + data.row[hzv] = data.version; } }); // If permissions returns a function, we need to use it to validate if (permissions()) { // For the set of rows to write, gather info for the validation step - return Promise.resolve(pre_validate(row_data.map((data) => data.row))) + return Promise.resolve(preValidate(rowData.map((data) => data.row))) .then((infos) => { - assert(infos.length === row_data.length); + assert(infos.length === rowData.length); // For each row to write (and info), validate it with permissions const validator = permissions(); if (validator) { - const valid_rows = []; - row_data.forEach((data, i) => { - const res = validate_row(validator, data.row, infos[i]); + const validRows = []; + rowData.forEach((data, i) => { + const res = validateRow(validator, data.row, infos[i]); if (res !== undefined) { - response_data[data.index] = res; + responseData[data.index] = res; } else { - valid_rows.push(data); + validRows.push(data); } }); - row_data = valid_rows; + rowData = validRows; } }); } }).then(() => { // For the set of valid rows, call the write step - if (row_data.length === 0) { + if (rowData.length === 0) { return []; } - return do_write(row_data.map((data) => data.row)).then((res) => res.changes); + return doWrite(rowData.map((data) => data.row)).then((res) => res.changes); }).then((changes) => { - assert(changes.length === row_data.length); + assert(changes.length === rowData.length); // Remove successful writes and invalidated writes that had an initial version - const retry_rows = []; - row_data.forEach((data, index) => { + const retryRows = []; + rowData.forEach((data, index) => { const res = changes[index]; if (res.error !== undefined) { if (res.error.indexOf('Duplicate primary key') === 0) { - response_data[data.index] = {error: 'The document already exists.'}; - } else if (res.error.indexOf(invalidated_msg) === 0 && + responseData[data.index] = {error: 'The document already exists.'}; + } else if (res.error.indexOf(invalidatedMsg) === 0 && data.version === undefined) { - retry_rows.push(data); + retryRows.push(data); } else { - response_data[data.index] = {error: res.error}; + responseData[data.index] = {error: res.error}; } } else if (res.new_val === null) { - response_data[data.index] = {id: res.old_val.id, [hz_v]: res.old_val[hz_v]}; + responseData[data.index] = {id: res.old_val.id, [hzv]: res.old_val[hzv]}; } else { - response_data[data.index] = {id: res.new_val.id, [hz_v]: res.new_val[hz_v]}; + responseData[data.index] = {id: res.new_val.id, [hzv]: res.new_val[hzv]}; } }); // Recurse, after which it will decide if there is more work to be done - first_attempt = false; - return iterate(retry_rows, response_data, deadline); + firstAttempt = false; + return iterate(retryRows, responseData, deadline); }); }; - return iterate(original_rows.map((row, index) => ({row, index, version: row[hz_v]})), - Array(original_rows.length).fill(null)) - .then(make_write_response); + return iterate(originalRows.map((row, index) => ({row, index, version: row[hzv]})), + Array(originalRows.length).fill(null)) + .then(makeWriteResponse); } -function validate_old_row_optional(validator, original, old_row, new_row) { - const expected_version = original[hz_v]; - if (expected_version !== undefined && - (!old_row || expected_version !== old_row[hz_v])) { - return new Error(invalidated_msg); - } else if (!validator(old_row, new_row)) { - return new Error(unauthorized_msg); +function validateOldRowOptional(validator, original, oldRow, newRow) { + const expectedVersion = original[hzv]; + if (expectedVersion !== undefined && + (!oldRow || expectedVersion !== oldRow[hzv])) { + return new Error(invalidatedMsg); + } else if (!validator(oldRow, newRow)) { + return new Error(unauthorizedMsg); } - if (old_row) { - const old_version = old_row[hz_v]; - if (expected_version === undefined) { - original[hz_v] = old_version === undefined ? -1 : old_version; + if (oldRow) { + const oldVersion = oldRow[hzv]; + if (expectedVersion === undefined) { + original[hzv] = oldVersion === undefined ? -1 : oldVersion; } } } -function validate_old_row_required(validator, original, old_row, new_row) { - if (old_row == null) { - return new Error(missing_msg); +function validateOldRowRequired(validator, original, oldRow, newRow) { + if (oldRow == null) { + return new Error(missingMsg); } - const old_version = old_row[hz_v]; - const expected_version = original[hz_v]; - if (expected_version !== undefined && - expected_version !== old_version) { - return new Error(invalidated_msg); - } else if (!validator(old_row, new_row)) { - return new Error(unauthorized_msg); + const oldVersion = oldRow[hzv]; + const expectedVersion = original[hzv]; + if (expectedVersion !== undefined && + expectedVersion !== oldVersion) { + return new Error(invalidatedMsg); + } else if (!validator(oldRow, newRow)) { + return new Error(unauthorizedMsg); } - if (expected_version === undefined) { - original[hz_v] = old_version === undefined ? -1 : old_version; + if (expectedVersion === undefined) { + original[hzv] = oldVersion === undefined ? -1 : oldVersion; } } module.exports = { - invalidated_msg, - missing_msg, - timeout_msg, - unauthorized_msg, - apply_version, - retry_loop, - validate_old_row_required, - validate_old_row_optional, - versionField: hz_v, + invalidatedMsg, + missingMsg, + timeoutMsg, + unauthorizedMsg, + applyVersion, + retryLoop, + validateOldRowRequired, + validateOldRowOptional, + versionField: hzv, }; diff --git a/plugins/above/.babelrc b/plugins/above/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/above/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/above/package.json b/plugins/above/package.json index b6822eb2f..f3befb466 100644 --- a/plugins/above/package.json +++ b/plugins/above/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/above", "version": "1.0.0", "description": "Plugin for the 'above' term in the Horizon Collections API.", - "main": "dist/above.js", + "main": "src/above.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/below/.babelrc b/plugins/below/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/below/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/below/package.json b/plugins/below/package.json index 3aa079de1..781ce51dd 100644 --- a/plugins/below/package.json +++ b/plugins/below/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/below", "version": "1.0.0", "description": "Plugin for the 'below' term in the Horizon Collections API.", - "main": "dist/below.js", + "main": "src/below.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/collection/.babelrc b/plugins/collection/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/collection/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/collection/package.json b/plugins/collection/package.json index 1ca4aa811..94fdb5eb7 100644 --- a/plugins/collection/package.json +++ b/plugins/collection/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/collection", "version": "1.0.0", "description": "Plugin for the 'collection' term in the Horizon Collections API.", - "main": "dist/collection.js", + "main": "src/collection.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/defaults/.babelrc b/plugins/defaults/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/defaults/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/defaults/package.json b/plugins/defaults/package.json index 300f90136..e9c8c94c9 100644 --- a/plugins/defaults/package.json +++ b/plugins/defaults/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/defaults", "version": "1.0.0", "description": "Contains all the default plugins for the Horizon Collections API.", - "main": "dist/defaults.js", + "main": "src/defaults.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/fetch/.babelrc b/plugins/fetch/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/fetch/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/fetch/package.json b/plugins/fetch/package.json index d6324c544..f96783ac9 100644 --- a/plugins/fetch/package.json +++ b/plugins/fetch/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/fetch", "version": "1.0.0", "description": "Plugin for the 'fetch' term in the Horizon Collections API.", - "main": "dist/fetch.js", + "main": "src/fetch.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/fetch/src/fetch.js b/plugins/fetch/src/fetch.js index 596278e08..d94711fb9 100644 --- a/plugins/fetch/src/fetch.js +++ b/plugins/fetch/src/fetch.js @@ -17,21 +17,23 @@ function fetch(context) { return reql.run(conn, reqlOptions) }).then((result) => { if (result !== null && result.constructor.name === 'Cursor') { - res.complete.then(() => { - result.close().catch(() => { }); - }); + const cleanup = () => feed.close().catch(() => {}); + res.complete.then(cleanup).catch(cleanup); + + // RSI: utility functions to make this easier? + res.write({op: 'replace', path: '', value: {type: 'value', synced: false, val: []}}); // TODO: reuse cursor batching return result.eachAsync((item) => { const validator = permissions(); if (validator && !validator(item)) { next(new Error('Operation not permitted.')); - result.close().catch(() => { }); + cleanup(); } else { - res.write([item]); + res.write({op: 'add', path: '/val/-', value: item}); } }).then(() => { - res.end(); + res.end({op: 'replace', path: '/synced', value: true}); }); } else { const validator = permissions(); @@ -39,15 +41,15 @@ function fetch(context) { if (validator) { for (const item of result) { if (!validator(item)) { - return next(new Error('Operation not permitted.')); + throw new Error('Operation not permitted.'); } } } - res.end(result); + res.end({op: 'replace', path: '', value: {type: 'value', synced: true, val: result}}); } else if (validator && !validator(result)) { next(new Error('Operation not permitted.')); } else { - res.end([result]); + res.end({op: 'replace', path: '', value: {type: 'value', synced: true, val: result}}); } } }).catch(next); diff --git a/plugins/find/.babelrc b/plugins/find/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/find/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/find/package.json b/plugins/find/package.json index 22a297474..c07d4a5a4 100644 --- a/plugins/find/package.json +++ b/plugins/find/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/find", "version": "1.0.0", "description": "Plugin for the 'find' term in the Horizon Collections API.", - "main": "dist/find.js", + "main": "src/find.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/findAll/.babelrc b/plugins/findAll/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/findAll/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/findAll/package.json b/plugins/findAll/package.json index 37283be0c..99e6d3f70 100644 --- a/plugins/findAll/package.json +++ b/plugins/findAll/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/findAll", "version": "1.0.0", "description": "Plugin for the 'findAll' term in the Horizon Collections API.", - "main": "dist/findAll.js", + "main": "src/findAll.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/insert/.babelrc b/plugins/insert/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/insert/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/insert/package.json b/plugins/insert/package.json index 161c05280..b9cbaba34 100644 --- a/plugins/insert/package.json +++ b/plugins/insert/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/insert", "version": "1.0.0", "description": "Plugin for the 'insert' term in the Horizon Collections API.", - "main": "dist/insert.js", + "main": "src/insert.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/insert/src/insert.js b/plugins/insert/src/insert.js index 8e0584fa1..cfde20aee 100644 --- a/plugins/insert/src/insert.js +++ b/plugins/insert/src/insert.js @@ -4,11 +4,11 @@ const {r} = require('@horizon/server'); const {reqlOptions, writes} = require('@horizon/plugin-utils'); function insert(context) { - return (request, response, next) => { + return (req, res, next) => { const conn = context.horizon.rdbConnection.connection(); - const timeout = request.getParameter('timeout'); - const collection = request.getParameter('collection'); - const permissions = request.getParameter('hz_permissions'); + const timeout = req.getParameter('timeout'); + const collection = req.getParameter('collection'); + const permissions = req.getParameter('hz_permissions'); if (!collection) { throw new Error('No collection given for insert operation.'); @@ -16,7 +16,7 @@ function insert(context) { throw new Error('No permissions given for insert operation.'); } - writes.retryLoop(request.options.insert, permissions, timeout, + writes.retryLoop(req.options.insert, permissions, timeout, (rows) => // pre-validation, all rows Array(rows.length).fill(null), (validator, row, info) => { // validation, each row @@ -29,7 +29,7 @@ function insert(context) { .insert(rows.map((row) => writes.applyVersion(r.expr(row), 0)), {returnChanges: 'always'}) .run(conn, reqlOptions) - ).then((msg) => response.end(msg)).catch(next); + ).then((patch) => res.end(patch)).catch(next); }; } diff --git a/plugins/limit/.babelrc b/plugins/limit/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/limit/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/limit/package.json b/plugins/limit/package.json index d78da6a91..5a71772a0 100644 --- a/plugins/limit/package.json +++ b/plugins/limit/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/limit", "version": "1.0.0", "description": "Plugin for the 'limit' term in the Horizon Collections API.", - "main": "dist/limit.js", + "main": "src/limit.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/order/.babelrc b/plugins/order/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/order/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/order/package.json b/plugins/order/package.json index fa4e5a5cb..60325237f 100644 --- a/plugins/order/package.json +++ b/plugins/order/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/order", "version": "1.0.0", "description": "Plugin for the 'order' term in the Horizon Collections API.", - "main": "dist/order.js", + "main": "src/order.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/permissions/.babelrc b/plugins/permissions/.babelrc deleted file mode 100644 index c13c5f627..000000000 --- a/plugins/permissions/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["es2015"] -} diff --git a/plugins/permissions/package.json b/plugins/permissions/package.json index 3618e0a0a..bb9910668 100644 --- a/plugins/permissions/package.json +++ b/plugins/permissions/package.json @@ -2,11 +2,10 @@ "name": "@horizon-plugins/permissions", "version": "1.0.0", "description": "Plugin adding user groups and permissions to Horizon.", - "main": "dist/permissions.js", + "main": "src/permissions.js", "scripts": { "lint": "eslint src", - "test": "mocha dist/test", - "build": "babel src -d dist -s true" + "test": "mocha src/test" }, "repository": { "type": "git", diff --git a/plugins/permissions/src/template.js b/plugins/permissions/src/template.js index 341b407a2..9b0ddc066 100644 --- a/plugins/permissions/src/template.js +++ b/plugins/permissions/src/template.js @@ -7,9 +7,60 @@ const vm = require('vm'); // RSI: where do we get the list of options from? there's no easy way to accept any // method - we could try to parse the ast of the javascript itself before evaluating // the template -const ast = require('@horizon/client/lib/ast'); -const validIndexValue = require('@horizon/client/lib/util/valid-index-value').default; -const {remakeError} = require('@horizon/plugin-utils'); +const {isObject, remakeError} = require('@horizon/plugin-utils'); + +const templateData = Symbol('templateData'); + +function templateCompare(query, template, context) { + if (template === undefined) { + return false; + } else if (template instanceof Any || + template instanceof AnyObject || + template instanceof AnyArray) { + if (!template.matches(query, context)) { + return false; + } + } else if (template instanceof UserId) { + if (query !== context.id) { + return false; + } + } else if (template === null) { + if (query !== null) { + return false; + } + } else if (Array.isArray(template)) { + if (!Array.isArray(query) || + template.length !== query.length) { + return false; + } + for (let i = 0; i < template.length; ++i) { + if (!templateCompare(query[i], template[i], context)) { + return false; + } + } + } else if (typeof template === 'object') { + if (typeof query !== 'object') { + return false; + } + + for (const key in query) { + if (!templateCompare(query[key], template[key], context)) { + return false; + } + } + + // Make sure all template keys were handled + for (const key in template) { + if (query[key] === undefined) { + return false; + } + } + } else if (template !== query) { + return false; + } + + return true; +} class Any { constructor(values) { @@ -86,166 +137,92 @@ class AnyArray { class UserId { } -const wrapWrite = (query, docs) => { - if (docs instanceof AnyArray || - Array.isArray(docs)) { - query.data = docs; - } else { - query.data = [docs]; +// The chained object in the template is of the format: +// { +// [templateData]: { +// any: , +// options: { +//