├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── TODO.md ├── bin └── rethinkdb-proxy.js ├── index.js ├── lib-test ├── changefeeds.js ├── components │ ├── buffer-parser.js │ └── rethinkdb-proxy.js ├── connection.js ├── database-and-table-access.js ├── edge-cases.js ├── index.js ├── parallel.js ├── parser.js ├── queries.js ├── read-only-queries.js └── utils.js ├── lib ├── buffer-parser.js ├── index.js ├── logger.js ├── options-parser.js └── query-parser.js ├── package.json ├── src ├── buffer-parser.js ├── index.js ├── logger.js ├── options-parser.js └── query-parser.js └── test ├── changefeeds.js ├── components ├── buffer-parser.js └── rethinkdb-proxy.js ├── connection.js ├── database-and-table-access.js ├── edge-cases.js ├── index.js ├── parallel.js ├── parser.js ├── queries.js ├── read-only-queries.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | !**/* 2 | 3 | node_modules 4 | driver 5 | fabfile.py 6 | **/*.pyc 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": false, 4 | "eqeqeq": true, 5 | "forin": true, 6 | "freeze": true, 7 | "immed": true, 8 | "indent": 2, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "nonbsp": true, 14 | "nonew": false, 15 | "quotmark": "single", 16 | "undef": true, 17 | 18 | "maxcomplexity": 10, 19 | "maxdepth": 4, 20 | "maxlen": 120, 21 | "maxparams": 5, 22 | "maxstatements": 25, 23 | 24 | "eqnull": true, 25 | "esnext": true, 26 | 27 | "node": true, 28 | "globals": { 29 | "describe": false, 30 | "xdescribe": false, 31 | "ddescribe": false, 32 | "it": false, 33 | "xit": false, 34 | "beforeEach": false, 35 | "afterEach": false, 36 | "before": false, 37 | "after": false 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.11" 5 | - "0.12" 6 | - "iojs" 7 | 8 | matrix: 9 | fast_finish: true 10 | 11 | before_install: 12 | - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list 13 | - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - 14 | - sudo apt-get update -q 15 | - sudo apt-get -y --force-yes install rethinkdb 16 | 17 | notifications: 18 | email: false 19 | 20 | before_script: 21 | - rethinkdb --daemon 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RethinkDB Proxy 2 | 3 | [![Build Status](https://travis-ci.org/thejsj/rethinkdb-proxy.svg?branch=master)](https://travis-ci.org/thejsj/rethinkdb-proxy) 4 | [![npm version](https://badge.fury.io/js/rethinkdb-proxy.svg)](http://badge.fury.io/js/rethinkdb-proxy) 5 | 6 | *Reverse proxy for RethinkDB* 7 | 8 | Make your RethinkDB publicly accessible through limiting what kind of queries 9 | can be executed on your RethinkDB database. 10 | 11 | Currently, RethinkDB has no access control (although they're currently 12 | [working on it](https://github.com/rethinkdb/rethinkdb/issues/4519)). Anyone 13 | with access to a running instance has access to everything, including system tables. 14 | This is a simple solution to that problem that allows for limited access to 15 | RethinkDB. 16 | 17 | - [Introduction by Example](#introduction-by-example-) 18 | - [Try it!](#try-it-) 19 | - [Installation](#installation) 20 | - [Running `rethinkdb-proxy`](#running-rethinkdb-proxy-) 21 | - [Options](#options-) 22 | - [The Future](#the-future-) 23 | - [License](#license-) 24 | 25 | ## Introduction by Example 26 | 27 | First, start the proxy. 28 | 29 | ```bash 30 | $ rethinkdb-proxy --port 8125 31 | ``` 32 | Using the proxy, getting all users in the `users` table is allowed. 33 | 34 | ```javascript 35 | r.connect({ port: 8125 }).then((conn) => { 36 | r.table('users').coerceTo('array').run(conn) 37 | .then((results) => { 38 | // We have some results! 39 | console.log(results); // [{ name: 'jorge' }, ... ] 40 | }); 41 | }); 42 | ``` 43 | 44 | But deleting the users **is not**: 45 | 46 | ```javascript 47 | import rethinkDBProxy from 'rethinkdb-proxy'; 48 | rethinkDBProxy({ port: 8125 }); 49 | 50 | r.connect({ port: 8125 }).then((conn) => { 51 | r.table('users').delete('array').run(conn) 52 | .catch((err) => { 53 | // We get an error! 54 | console.log(err); // RqlClientError: Cannot execute query. "DELETE" query not allowed 55 | }); 56 | }); 57 | ``` 58 | 59 | ## Try it! 60 | 61 | You can try out rethinkdb-proxy by connecting to a publicly available proxy at `rethinkdb-proxy.thejsj.com:8125`. 62 | This database (named `test`) has two tables: `countries` and `cities`. You can 63 | run queries against it to see how `rethindkb-proxy` works. 64 | 65 | **JavasScript:** 66 | 67 | ```javascript 68 | import r from 'rethinkdb'; 69 | r.connect({ host: 'rethinkdb-proxy.thejsj.com', port: 8125 }) 70 | .then(function (conn) { 71 | r.table('countries').coerceTo('array').run(conn); 72 | }); 73 | ``` 74 | 75 | **Python:** 76 | 77 | ```python 78 | import rethinkdb as r 79 | conn = r.connect(host="rethinkdb-proxy.thejsj.com", port=8125) 80 | r.table('countries').coerce_to('array').run(conn) 81 | ``` 82 | 83 | ## Installation 84 | 85 | Install rethinkdb-proxy through [npm](https://www.npmjs.com/package/rethinkdb-proxy). 86 | 87 | ``` 88 | npm install -g rehtinkdb-proxy 89 | ``` 90 | 91 | ## Running rethinkdb-proxy 92 | 93 | #### CLI 94 | 95 | rethinkdb-proxy comes with a CLI out-of-the box: 96 | 97 | ```javascript 98 | rethinkdb-proxy --port 8125 99 | ``` 100 | 101 | #### Module 102 | 103 | You can also import rethinkdb-proxy into Node.js: 104 | 105 | ```javascript 106 | import rethinkDBProxy from 'rethinkdb-proxy'; 107 | rethinkDBProxy({ port: 8125, allowInsert: true }); 108 | ``` 109 | 110 | ## Options 111 | 112 | - [`port`](#port) 113 | - [`rdbHost`](#rdbHost) 114 | - [`rdbPort`](#rdbPort) 115 | - [`dbs`](#dbs) 116 | - [`allowSysDbAccess`](#allowSysDbAccess) 117 | - [`tables`](#tables) 118 | - [`allowWrites`](#allowWrites) 119 | - [`allowInsert`](#allowInsert) 120 | - [`allowUpdate`](#allowUpdate) 121 | - [`allowDelete`](#allowDelete) 122 | - [`allowReplace`](#allowReplace) 123 | - [`allowDbCreate`](#allowDbCreate) 124 | - [`allowDbDrop`](#allowDbDrop) 125 | - [`allowTableCreate`](#allowTableCreate) 126 | - [`allowTableDrop`](#allowTableDrop) 127 | - [`allowIndexes`](#allowIndexes) 128 | - [`allowIndexCreate`](#allowIndexCreate) 129 | - [`allowIndexDrop`](#allowIndexDrop) 130 | - [`allowIndexRename`](#allowIndexRename) 131 | - [`allowReconfigure`](#allowReconfigure) 132 | - [`allowRebalance`](#allowRebalance) 133 | - [`allowHttp`](#allowHttp) 134 | - [`allowJavascript`](#allowJavascript) 135 | 136 | ### Port 137 | 138 | | Module Parameter | CLI Parameter | Default | API Reference | 139 | |--------------------|-------------------------|-------------|---------------| 140 | | `port` | `--port` | `8125` | | 141 | 142 | Port in which to listen for driver connections. You should point your driver to this port. 143 | 144 | ### RethinkDB Host 145 | 146 | | Module Parameter | CLI Parameter | Default | API Reference | 147 | |--------------------|-------------------------|-------------|-------------------------------------------------------------| 148 | | `rdbHost` | `--rdb-host` | `localhost` | [connect](http://www.rethinkdb.com/api/javascript/connect/) | 149 | 150 | Host in which RethinkDB is running. 151 | 152 | ### RethinkDB Port 153 | 154 | | Module Parameter | CLI Parameter | Default | API Reference | 155 | |--------------------|-------------------------|-------------|--------------------------------------------------------------| 156 | | `rdbPort` | `--rdb-host` | `localhost` | [connect](http://www.rethinkdb.com/api/javascript/connect/) | 157 | 158 | Host in which RethinkDB is running. 159 | 160 | ### Databases 161 | 162 | | Module Parameter | CLI Parameter | Default | API Reference | 163 | |--------------------|-------------------------|-------------|---------------| 164 | | `dbs` | `--dbs` | `[ ]` | | 165 | 166 | Database to allow access to. By default, all database are allowed except `rethinkdb`. 167 | 168 | ### Allow System Database Access 169 | 170 | | Module Parameter | CLI Parameter | Default | API Reference | 171 | |--------------------|-------------------------|-------------|---------------| 172 | | `allowSysDbAccess` | `--allow-sys-db-access` | `false` | | 173 | 174 | Allow access to the `rethinkdb` database. This is not allowed by default because 175 | access to this database allows the user to delete all other data, cancel jobs, 176 | mess with the cluster, etc. 177 | 178 | ### Tables 179 | 180 | | Module Parameter | CLI Parameter | Default | API Reference | 181 | |--------------------|-------------------------|-------------|---------------| 182 | | `tables` | `--tables` | `[ ]` | | 183 | 184 | Tables to allow access to. Tables must include their database `db.table`. 185 | 186 | ### Allow Writes 187 | 188 | | Module Parameter | CLI Parameter | Default | API Reference | 189 | |--------------------|-------------------------|-------------|---------------| 190 | | `allowWrites` | `--allow-writes` | `false` | | 191 | 192 | Allow all operations that write to the database (`insert`, `update`, `delete`). 193 | 194 | ### Allow `insert` 195 | 196 | | Module Parameter | CLI Parameter | Default | API Reference | 197 | |--------------------|-------------------------|-------------|-----------------------------------------------------------| 198 | | `allowInsert` | `--allow-insert` | `false` | [insert](http://www.rethinkdb.com/api/javascript/insert/) | 199 | 200 | Allow `insert` queries. 201 | 202 | ### Allow `update` 203 | 204 | | Module Parameter | CLI Parameter | Default | API Reference | 205 | |--------------------|-------------------------|-------------|-----------------------------------------------------------| 206 | | `allowUpdate` | `--allow-update` | `false` | [update](http://www.rethinkdb.com/api/javascript/update/) | 207 | 208 | Allow `update` queries. 209 | 210 | ### Allow `delete` 211 | 212 | | Module Parameter | CLI Parameter | Default | API Reference | 213 | |--------------------|-------------------------|-------------|-----------------------------------------------------------| 214 | | `allowDelete` | `--allow-delete` | `false` | [delete](http://www.rethinkdb.com/api/javascript/delete/) | 215 | 216 | Allow `delete` queries. 217 | 218 | ### Allow `replace` 219 | 220 | | Module Parameter | CLI Parameter | Default | API Reference | 221 | |--------------------|-------------------------|-------------|------------------------------------------------------------| 222 | | `allowReplace` | `--allow-replace` | `false` | [replace](http://www.rethinkdb.com/api/javascript/delete/) | 223 | 224 | Allow `replace` queries. 225 | 226 | ### Allow `dbCreate` 227 | 228 | | Module Parameter | CLI Parameter | Default | API Reference | 229 | |--------------------|-------------------------|-------------|----------------------------------------------------------------| 230 | | `allowDbCreate` | `--allow-db-create` | `false` | [dbCreate](http://www.rethinkdb.com/api/javascript/db_create/) | 231 | 232 | Allow `dbCreate` queries 233 | 234 | ### Allow `dbDrop` 235 | 236 | | Module Parameter | CLI Parameter | Default | API Reference | 237 | |--------------------|-------------------------|-------------|------------------------------------------------------------| 238 | | `allowDbDrop` | `--allow-db-drop` | `false` | [dbDrop](http://www.rethinkdb.com/api/javascript/db_drop/) | 239 | 240 | Allow `dbDrop` queries 241 | 242 | ### Allow `tableCreate` 243 | 244 | | Module Parameter | CLI Parameter | Default | API Reference | 245 | |--------------------|-------------------------|-------------|----------------------------------------------------------------------| 246 | | `allowTableCreate` | `--allow-table-create` | `false` | [tableCreate](http://www.rethinkdb.com/api/javascript/table_create/) | 247 | 248 | Allow `tableCreate` queries. 249 | 250 | ### Allow `tableDrop` 251 | 252 | | Module Parameter | CLI Parameter | Default | API Reference | 253 | |--------------------|-------------------------|-------------|-------------------------------------------------------------------| 254 | | `allowTableDrop` | `--allow-table-drop` | `false` | [tableDrop](http://www.rethinkdb.com/api/javascript/table_drop/) | 255 | 256 | Allow `tableDrop` queries. 257 | 258 | ### Allow Indexes 259 | 260 | | Module Parameter | CLI Parameter | Default | API Reference | 261 | |--------------------|-------------------------|-------------|---------------| 262 | | `allowIndexes` | `--allow-indexes` | `false` | | 263 | 264 | Allow all operations on indexes (`indexCreate`, `indexDrop`, `indexRename`). 265 | 266 | ### Allow `indexCreate` 267 | 268 | | Module Parameter | CLI Parameter | Default | API Reference | 269 | |--------------------|-------------------------|-------------|----------------------------------------------------------------------| 270 | | `allowIndexCreate` | `--allow-index-create` | `false` | [indexCreate](http://www.rethinkdb.com/api/javascript/index_create/) | 271 | 272 | Allow `indexCreate` queries. 273 | 274 | ### Allow `indexDrop` 275 | 276 | | Module Parameter | CLI Parameter | Default | API Reference | 277 | |--------------------|-------------------------|-------------|------------------------------------------------------------------| 278 | | `allowIndexDrop` | `--allow-index-drop` | `false` | [indexDrop](http://www.rethinkdb.com/api/javascript/index_drop/) | 279 | 280 | Allow `indexDrop` queries. 281 | 282 | ### Allow `indexRename` 283 | 284 | | Module Parameter | CLI Parameter | Default | API Reference | 285 | |--------------------|-------------------------|-------------|----------------------------------------------------------------------| 286 | | `allowIndexRename` | `--allow-index-rename` | `false` | [indexRename](http://www.rethinkdb.com/api/javascript/index_rename/) | 287 | 288 | Allow `indexRename` queries. 289 | 290 | ### Allow `reconfigure` 291 | 292 | | Module Parameter | CLI Parameter | Default | API Reference | 293 | |--------------------|-------------------------|-------------|---------------------------------------------------------------------| 294 | | `allowReconfigure` | `--allow-reconfigure` | `false` | [reconfigure](http://www.rethinkdb.com/api/javascript/reconfigure/) | 295 | 296 | Allow `reconfigure` queries. 297 | 298 | ### Allow `rebalance` 299 | 300 | | Module Parameter | CLI Parameter | Default | API Reference | 301 | |--------------------|-------------------------|-------------|----------------------------------------------------------------------| 302 | | `allowRebalance` | `--allow-rebalance` | `false` | [rebalance](http://www.rethinkdb.com/api/javascript/rebalance/) | 303 | 304 | Allow `rebalance` queries. 305 | 306 | ### Allow `http` 307 | 308 | | Module Parameter | CLI Parameter | Default | API Reference | 309 | |--------------------|-------------------------|-------------|-------------------------------------------------------| 310 | | `allowHttp` | `--allow-http` | `false` | [http](http://www.rethinkdb.com/api/javascript/http/) | 311 | 312 | Allow queries with the `http` term. 313 | 314 | ### Allow `js` 315 | 316 | | Module Parameter | CLI Parameter | Default | API Reference | 317 | |--------------------|-------------------------|-------------|----------------------------------------------------| 318 | | `allowJavascript` | `--allow-javascript` | `false` | [js](http://www.rethinkdb.com/api/javascript/js/) | 319 | 320 | Allow queries with the `js` term. 321 | 322 | ### The Future 323 | 324 | As of right now, there are many features that could be added to rethinkdb-proxy. 325 | If you have any suggestions, please [submit an issue](https://github.com/thejsj/rethinkdb-proxy/issues). 326 | If enough people use this, I'd be happy to implement them. Features for the 327 | future might include: 328 | 329 | - Access from the front-end, Firebase style (through http and/or websockets) 330 | - Authentication/User accounts (perhaps integration with Github/OAuth) 331 | - More robust access control (permissions per database, per table) 332 | - Options stored in the database 333 | 334 | ### License 335 | 336 | Copyright (c) 2015, [Jorge Silva](mailto:jorge.silva@thejsj.com). 337 | 338 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 339 | 340 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 341 | 342 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 343 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - [x] README 4 | - [x] Wrap allow around code 5 | - [x] Add api links to docs 6 | - [x] Errors 7 | - [x] Add error handling for non-rethinkdb driver connections 8 | - [x] Keep server running on error 9 | - [x] It should handle queries that throw errors 10 | - [x] Fix `ECONNRESET` bugs 11 | 12 | ## Future 13 | 14 | - [ ] Use from browser 15 | - [ ] Replace `net` library with websockets 16 | - [ ] Authentication 17 | - [ ] Convert queries back to strings. Re-build AST. 18 | - [ ] Save options in RethinkDB instead of passing them as flags 19 | - [ ] Add logging 20 | 21 | ### Done 22 | 23 | - [x] Test cursors 24 | - [x] Test multiple concurrent queries from the same connection 25 | - [x] Test changefeeds 26 | - [x] API 27 | - [x] Better name for allowSysDbAccess 28 | - [x] Allow only a single database/table 29 | - [x] database `--db test,hello` 30 | - [x] Prevent connection if database is passed that is not allowed (Throw error) 31 | - [x] tables `--table people, trains`, `--table test.people, hello.trains` 32 | - [x] Prevent `r.args` 33 | - [x] Prevent getting the dbName, tableName dynamically 34 | - [x] Refactor 35 | - [x] Refactor code so that it uses classes 36 | - [x] Break up codes into more modular structure 37 | - [x] Opt-in Options: 38 | - [x] Allow Insert `--allow-insert` 39 | - [x] Allow Delete`--allow-delete` 40 | - [x] Allow Update`--allow-update` 41 | - [x] Allow Database Create `--allow-db-create` 42 | - [x] Database Drop `--allow-db-drop` 43 | - [x] Table Creation `--allow-table-create` 44 | - [x] Table Drop `--allow-table-drop` 45 | - [x] HTTP `--allow-http` 46 | - [x] JavaScript `--allow-javascript` 47 | - [x] Reconfigure `--allow-reconfigure` 48 | - [x] Rebalance `--allow-rebalance` 49 | - [x] Add token/connection parser 50 | - [x] Prevent `insert`, `update`, `replace`, `delete` 51 | - [x] Fix corner cases 52 | - [x] `update` with `null`: does nothing. 53 | - [x] `update` with `r.literal()`: throws Error. 54 | - [x] `insert` with `conflict: replace`. `allowReplace` must be allowed. 55 | - [x] `insert` with `conflict: replace` with `null` will deleted the document. `allowDelete` must be allowed. (Throw error) 56 | - [x] `insert` with `conflict: update`. `allowUpdate` must be allowed. 57 | - [x] `replace` with `null` deletes the document. 58 | - [x] `replace` with `r.literal`: Throws error. 59 | 60 | -------------------------------------------------------------------------------- /bin/rethinkdb-proxy.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | var cli = require('cli').enable('status'); 3 | var taser = require('taser'); 4 | var RethinkDBProxy = require('../lib/'); 5 | 6 | cli.parse({ 7 | 'port': [false, 'Port in which to listen for driver connections', 'number', null], 8 | 'rdbHost': [false, 'RethinkDB host to connect to', 'string', null], 9 | 'rdbPort': [false, 'RethinkDB port to connect to', 'number', null], 10 | 'log-level': [false, 'Log level (Default for `in`, `out`, and `sys`)', 'string', 'fatal'], 11 | 'log-level-in': [false, 'Log level for incoming messages', 'string', 'fatal'], 12 | 'log-level-out': [false, 'Log level for outgoing messages', 'string', 'fatal'], 13 | 'log-level-sys': [false, 'Log level for system logs', 'string', 'fatal'], 14 | 'dbs': [false, 'Databases allowed', 'string', null], 15 | 'allow-sys-db-acces': [false, 'Allow access to the `rethinkdb` system database', 'string', null], 16 | 'tables': [false, 17 | 'Tables allowed. Must include dot (`db.table`) if multiple database allowed.', 'string', null], 18 | 'allow-writes': [false, 'Allow all operations that write to the database (`insert`, `update`, `delete`)'], 19 | 'allow-insert': [false, 'Allow `insert` queries'], 20 | 'allow-update': [false, 'Allow `update` queries'], 21 | 'allow-delete': [false, 'Allow `delete` queries'], 22 | 'allow-replace': [false, 'Allow `replace` queries'], 23 | 'allow-db-create': [false, 'Allow `dbCreate` queries'], 24 | 'allow-db-drop': [false, 'Allow `dbDrop` queries'], 25 | 'allow-table-create': [false, 'Allow `tableCreate` queries'], 26 | 'allow-table-drop': [false, 'Allow `tableDrop` queries'], 27 | 'allow-indexes': [false, 'Allow all operations on indexes (`indexCreate`, `indexDrop`, `indexRename`)'], 28 | 'allow-index-create': [false, 'Allow `indexCreate` queries'], 29 | 'allow-index-drop': [false, 'Allow `indexDrop` queries'], 30 | 'allow-index-rename': [false, 'Allow `indexRename` queries'], 31 | 'allow-reconfigure': [false, 'Allow `reconfigure` queries'], 32 | 'allow-rebalance': [false, 'Allow `rebalance` queries'], 33 | 'allow-http': [false, 'Allow queries with the `http` term'], 34 | 'allow-javascript': [false, 'Allow queries with the `js` term'] 35 | }); 36 | 37 | cli.main(function (args, _opts) { 38 | var opts = {}; 39 | for (var key in _opts) { 40 | if (_opts.hasOwnProperty(key) && _opts[key] !== null) { 41 | opts[key.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); })] = _opts[key]; 42 | } 43 | } 44 | var server = new RethinkDBProxy(opts); 45 | server.listen(opts.port); 46 | }); 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | var RethinkDBProxy = require('./lib/'); 3 | var server = new RethinkDBProxy({}); 4 | server.listen(); 5 | -------------------------------------------------------------------------------- /lib-test/changefeeds.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var _utils = require('./utils'); 23 | 24 | var _node_modulesRethinkdbProtoDef = require('../node_modules/rethinkdb/proto-def'); 25 | 26 | var _node_modulesRethinkdbProtoDef2 = _interopRequireDefault(_node_modulesRethinkdbProtoDef); 27 | 28 | var proxyPort = 8125; 29 | var dbName = 'rethinkdb_proxy_test'; 30 | var secondDbName = 'rethinkdb_proxy_test_2'; 31 | var tableName = 'entries'; 32 | var server = undefined; 33 | 34 | var executeQuery = (0, _utils.makeExecuteQuery)(dbName, proxyPort); 35 | var executeProxyQuery = (0, _utils.makeExecuteProxyQuery)(dbName, proxyPort); 36 | var assertQuery = (0, _utils.makeAssertQuery)(executeQuery); 37 | var createDatabase = (0, _utils.makeCreateDatabase)(dbName, tableName); 38 | var createSecondDatabase = (0, _utils.makeCreateDatabase)(secondDbName, tableName); 39 | var dropDatabase = (0, _utils.makeDropDatabase)(dbName); 40 | var throwError = function throwError(res) { 41 | throw new Error(); 42 | }; 43 | var expectError = function expectError(errorName, errorMessageMatch, err) { 44 | if (errorName !== null) errorName.should.equal(err.name); 45 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 46 | (err instanceof Error).should.equal(true); 47 | }; 48 | var closeCursorAndConn = function closeCursorAndConn(cursor, conn) { 49 | return cursor.close().then(function () { 50 | return conn.close(); 51 | }); 52 | }; 53 | var ce = function ce(err) { 54 | console.log('Error'); 55 | console.log(err); 56 | throw err; 57 | }; 58 | 59 | describe('Changefeeds', function () { 60 | 61 | before(function (done) { 62 | this.timeout(5000); 63 | createDatabase().then(function () { 64 | return new _bluebird2['default'](function (resolve, reject) { 65 | server = new _componentsRethinkdbProxy2['default']({ 66 | port: proxyPort, 67 | allowInsert: true 68 | }); 69 | server.listen(resolve); 70 | }); 71 | }).nodeify(done); 72 | }); 73 | 74 | after(function (done) { 75 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 76 | }); 77 | 78 | it('should listen for changes', function (done) { 79 | this.timeout(10000); 80 | var count = 0, 81 | results = [], 82 | cursor; 83 | // HINT: You'll need to pass an anonymous function to `filter` 84 | _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 85 | return _rethinkdb2['default'].table(tableName).changes().run(conn).then(function (_cursor) { 86 | cursor = _cursor; 87 | cursor.each(function (err, row) { 88 | count += 1; 89 | results.push(row); 90 | }); 91 | }).then(function () { 92 | return _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 93 | return _bluebird2['default'].all([_rethinkdb2['default'].table(tableName).insert({ name: 'SomeCountryName', population: 1 }).run(conn)['catch'](ce), _rethinkdb2['default'].table(tableName).insert({ name: 'Transylvania', population: 2000 }).run(conn)['catch'](ce), _rethinkdb2['default'].table(tableName).insert({ name: 'Hong Kong', population: 1500 }).run(conn), _rethinkdb2['default'].table(tableName).insert({ name: 'Bavaira', population: 98 }).run(conn)]).then(conn.close.bind(conn)); 94 | }); 95 | }).then(function () { 96 | return new _bluebird2['default'](function (resolve, reject) { 97 | setTimeout(function () { 98 | count.should.equal(4); 99 | results.should.be.instanceOf(Array); 100 | results = results.sort(function (a, b) { 101 | return a.new_val.population - b.new_val.population; 102 | }); 103 | results[0].new_val.population.should.equal(1); 104 | results[1].new_val.population.should.equal(98); 105 | results[0].new_val.should.have.property('name'); 106 | results[1].new_val.should.have.property('name'); 107 | resolve(); 108 | }, 50); // Hopefully, this is enough 109 | }); 110 | }).then(function () { 111 | return closeCursorAndConn(cursor, conn); 112 | }).nodeify(done); 113 | }); 114 | }); 115 | 116 | it('should listen for multiple changesfeeds', function (done) { 117 | this.timeout(10000); 118 | var first_count = 0, 119 | second_count = 0, 120 | results = [], 121 | cursor; 122 | return _bluebird2['default'].all([_rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 123 | return _rethinkdb2['default'].table(tableName).filter(_rethinkdb2['default'].row('population').lt(500)).changes().run(conn).then(function (cursor) { 124 | cursor.each(function (err, row) { 125 | first_count += 1; 126 | results.push(row); 127 | }, conn.close.bind(conn)); 128 | }); 129 | }), _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 130 | return _rethinkdb2['default'].table(tableName).filter(_rethinkdb2['default'].row('population').gt(500)).changes().run(conn).then(function (cursor) { 131 | cursor.each(function (err, row) { 132 | second_count += 1; 133 | results.push(row); 134 | }, conn.close.bind(conn)); 135 | }); 136 | })]).then(function () { 137 | return _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 138 | return _bluebird2['default'].all([_rethinkdb2['default'].table(tableName).insert({ name: 'SomeCountryName', population: 1 }).run(conn)['catch'](ce), _rethinkdb2['default'].table(tableName).insert({ name: 'Transylvania', population: 2000 }).run(conn)['catch'](ce), _rethinkdb2['default'].table(tableName).insert({ name: 'Hong Kong', population: 1500 }).run(conn), _rethinkdb2['default'].table(tableName).insert({ name: 'Bavaira', population: 98 }).run(conn)]).then(conn.close.bind(conn)); 139 | }); 140 | }).then(function () { 141 | return new _bluebird2['default'](function (resolve, reject) { 142 | setTimeout(function () { 143 | first_count.should.equal(2); 144 | second_count.should.equal(2); 145 | results.should.be.instanceOf(Array); 146 | results = results.sort(function (a, b) { 147 | return a.new_val.population - b.new_val.population; 148 | }); 149 | results[0].new_val.population.should.equal(1); 150 | results[1].new_val.population.should.equal(98); 151 | results[0].new_val.should.have.property('name'); 152 | results[1].new_val.should.have.property('name'); 153 | resolve(); 154 | }, 50); // Hopefully, this is enough 155 | }); 156 | }).nodeify(done); 157 | }); 158 | }); -------------------------------------------------------------------------------- /lib-test/components/buffer-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | var BufferParser; 7 | if (process.env.NODE_ENV === 'test_es6') { 8 | // Called from /test 9 | var BufferParserES6 = require('../../src/buffer-parser'); 10 | BufferParser = BufferParserES6; 11 | } else { 12 | // Called from /lib/test 13 | var BufferParserES5 = require('../../lib/buffer-parser'); 14 | BufferParser = BufferParserES5; 15 | } 16 | exports['default'] = BufferParser; 17 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib-test/components/rethinkdb-proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | var RethinkDBProxy; 7 | if (process.env.NODE_ENV === 'test_es6') { 8 | // Called from /test 9 | var RethinkDBProxyES6 = require('../../src'); 10 | RethinkDBProxy = RethinkDBProxyES6; 11 | } else { 12 | // Called from /lib/test 13 | var RethinkDBProxyES5 = require('../../lib'); 14 | RethinkDBProxy = RethinkDBProxyES5; 15 | } 16 | exports['default'] = RethinkDBProxy; 17 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib-test/connection.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var proxyPort = 8125; 23 | var server = undefined; 24 | 25 | describe('Connection', function () { 26 | 27 | before(function (done) { 28 | server = new _componentsRethinkdbProxy2['default']({ 29 | port: proxyPort 30 | }); 31 | server.listen(done); 32 | }); 33 | 34 | after(function (done) { 35 | server.close().then(done.bind(null, null)); 36 | }); 37 | 38 | it('should create a connection successfully', function (done) { 39 | _bluebird2['default'].resolve().then(function () { 40 | return [_rethinkdb2['default'].connect(), _rethinkdb2['default'].connect({ port: proxyPort })]; 41 | }).spread(function (connA, connB) { 42 | connA.port.should.equal(28015); 43 | connB.port.should.equal(proxyPort); 44 | connA.constructor.should.equal(connB.constructor); 45 | done(); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /lib-test/database-and-table-access.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var _utils = require('./utils'); 23 | 24 | var _node_modulesRethinkdbProtoDef = require('../node_modules/rethinkdb/proto-def'); 25 | 26 | var _node_modulesRethinkdbProtoDef2 = _interopRequireDefault(_node_modulesRethinkdbProtoDef); 27 | 28 | var proxyPort = 8125; 29 | var dbName = 'rethinkdb_proxy_test'; 30 | var secondDbName = 'rethinkdb_proxy_test_2'; 31 | var tableName = 'entries'; 32 | var server = undefined; 33 | 34 | var executeQuery = (0, _utils.makeExecuteQuery)(dbName, proxyPort); 35 | var executeProxyQuery = (0, _utils.makeExecuteProxyQuery)(dbName, proxyPort); 36 | var assertQuery = (0, _utils.makeAssertQuery)(executeQuery); 37 | var createDatabase = (0, _utils.makeCreateDatabase)(dbName, tableName); 38 | var createSecondDatabase = (0, _utils.makeCreateDatabase)(secondDbName, tableName); 39 | var dropDatabase = (0, _utils.makeDropDatabase)(dbName); 40 | var throwError = function throwError(res) { 41 | throw new Error(); 42 | }; 43 | var expectError = function expectError(errorName, errorMessageMatch, err) { 44 | if (errorName !== null) errorName.should.equal(err.name); 45 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 46 | (err instanceof Error).should.equal(true); 47 | }; 48 | 49 | describe('Database and Table Access', function () { 50 | 51 | var get = _rethinkdb2['default'].db(dbName).table(tableName); 52 | 53 | describe('allowSysDbAccess', function () { 54 | 55 | describe('Disallow RethinkDB Database Access', function () { 56 | 57 | before(function (done) { 58 | this.timeout(10000); 59 | createDatabase().then(function () { 60 | return new _bluebird2['default'](function (resolve, reject) { 61 | server = new _componentsRethinkdbProxy2['default']({ 62 | port: proxyPort 63 | }); 64 | server.listen(resolve); 65 | }); 66 | }).nodeify(done); 67 | }); 68 | 69 | after(function (done) { 70 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 71 | }); 72 | 73 | it('should not allow a query that uses the `rethinkdb` database', function (done) { 74 | executeQuery(_rethinkdb2['default'].db('rethinkdb').tableList()).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 75 | }); 76 | 77 | it('should not allow a query that uses the `rethinkdb` database inside a `do`', function (done) { 78 | executeQuery(_rethinkdb2['default'].expr(true)['do'](function () { 79 | return _rethinkdb2['default'].db('rethinkdb').tableList(); 80 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 81 | }); 82 | 83 | it('should not allow a query that uses the `rethinkdb` database inside a `do` inside a `do`', function (done) { 84 | executeQuery(_rethinkdb2['default'].expr([1, 2, 3])['do'](function (arr) { 85 | return _rethinkdb2['default'].expr(arr)['do'](function () { 86 | return _rethinkdb2['default'].db('rethinkdb').tableList(); 87 | }); 88 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 89 | }); 90 | 91 | it('should not allow a query that passes null to `replace` if `delete` is not allowed', function (done) { 92 | executeQuery(_rethinkdb2['default'].db('rethinkdb').table('server_config')).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 93 | }); 94 | }); 95 | 96 | describe('Allow RethinkDB Database Access', function () { 97 | 98 | before(function (done) { 99 | this.timeout(10000); 100 | createDatabase().then(function () { 101 | return new _bluebird2['default'](function (resolve, reject) { 102 | server = new _componentsRethinkdbProxy2['default']({ 103 | port: proxyPort, 104 | allowSysDbAccess: true 105 | }); 106 | server.listen(resolve); 107 | }); 108 | }).nodeify(done); 109 | }); 110 | 111 | after(function (done) { 112 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 113 | }); 114 | 115 | it('should allow a query that uses the `rethinkdb` database, if explicitly set', function (done) { 116 | executeQuery(_rethinkdb2['default'].db('rethinkdb').tableList()).then(function (res) { 117 | Array.isArray(res).should.equal(true); 118 | res.length.should.not.be.equal(0); 119 | }).nodeify(done); 120 | }); 121 | 122 | it('should allow a query queries the `rehtinkdb` database if explicitly set', function (done) { 123 | executeQuery(_rethinkdb2['default'].db('rethinkdb').table('server_config').coerceTo('array')).then(function (res) { 124 | Array.isArray(res).should.equal(true); 125 | res.length.should.not.be.equal(0); 126 | }).nodeify(done); 127 | }); 128 | }); 129 | }); 130 | 131 | describe('Database Access', function () { 132 | before(function (done) { 133 | this.timeout(10000); 134 | createDatabase().then(createSecondDatabase).then(function () { 135 | return new _bluebird2['default'](function (resolve, reject) { 136 | server = new _componentsRethinkdbProxy2['default']({ 137 | port: proxyPort, 138 | dbs: [dbName, 'someOtherDb'] 139 | }); 140 | server.listen(resolve); 141 | }); 142 | }).nodeify(done); 143 | }); 144 | 145 | after(function (done) { 146 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 147 | }); 148 | 149 | it('should not allow access to a database that was not allowed', function (done) { 150 | executeProxyQuery(_rethinkdb2['default'].db(secondDbName).tableList()).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 151 | }); 152 | 153 | it('should not allow access to a database that was not allowed inside a `do`', function (done) { 154 | executeProxyQuery(_rethinkdb2['default'].expr(1)['do'](function () { 155 | return _rethinkdb2['default'].db(secondDbName).tableList(); 156 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 157 | }); 158 | 159 | it('should not allow access to a database that was not allowed inside a `do`', function (done) { 160 | executeProxyQuery(_rethinkdb2['default'].expr(true)['do'](function () { 161 | return [_rethinkdb2['default'].db(secondDbName).tableList(), 4, 5, 9]; 162 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 163 | }); 164 | 165 | it('should allow access to a database that is allowed', function (done) { 166 | executeProxyQuery(_rethinkdb2['default'].db(dbName).tableList()).then(function (list) { 167 | list.should.be['instanceof'](Array); 168 | }).nodeify(done); 169 | }); 170 | 171 | describe('Connection', function () { 172 | it('should throw an error when trying to connect with an unallowed database', function (done) { 173 | _rethinkdb2['default'].connect({ port: proxyPort, db: secondDbName }).then(function (conn) { 174 | return _rethinkdb2['default'].tableList().run(conn); 175 | }).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 176 | }); 177 | }); 178 | 179 | describe('Query arguments', function () { 180 | 181 | it('should not allow a database name to be passed through r.args', function (done) { 182 | // For some reason, using `executeProxyQuery` doesn't work... 183 | _rethinkdb2['default'].connect({ port: proxyPort }).then(function (conn) { 184 | return _rethinkdb2['default'].db(_rethinkdb2['default'].args([dbName])).tableList().run(conn).then(conn.close.bind(conn)); 185 | }).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 186 | }); 187 | 188 | it('should allow a database name to be passed through r.expr', function (done) { 189 | executeProxyQuery(_rethinkdb2['default'].db(_rethinkdb2['default'].expr(dbName)).tableList()).then(function (list) { 190 | list.should.be['instanceof'](Array); 191 | }).nodeify(done); 192 | }); 193 | }); 194 | }); 195 | 196 | describe('Table Access', function () { 197 | 198 | before(function (done) { 199 | this.timeout(10000); 200 | createDatabase().then(createSecondDatabase).then(function () { 201 | return new _bluebird2['default'](function (resolve, reject) { 202 | server = new _componentsRethinkdbProxy2['default']({ 203 | port: proxyPort, 204 | dbs: [dbName, 'someOtherDb'], 205 | tables: dbName + '.' + tableName 206 | }); 207 | server.listen(resolve); 208 | }); 209 | }).nodeify(done); 210 | }); 211 | 212 | after(function (done) { 213 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 214 | }); 215 | 216 | it('should not allow access to a table if not allowed', function (done) { 217 | this.timeout(5000); 218 | _rethinkdb2['default'].connect({ db: dbName }).then(function (conn) { 219 | return _rethinkdb2['default'].tableCreate('someOtherTable').run(conn).then(function () { 220 | return conn.close(); 221 | }); 222 | }).then(function () { 223 | return executeProxyQuery(_rethinkdb2['default'].db(dbName).table('someOtherTable')).then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)).nodeify(done); 224 | }); 225 | }); 226 | 227 | it('should allow access to a table if allowed', function (done) { 228 | executeProxyQuery(_rethinkdb2['default'].db(dbName).table(tableName)).then(function (list) { 229 | list.should.be['instanceof'](Array); 230 | }).nodeify(done); 231 | }); 232 | 233 | describe('Connection', function () { 234 | it('should not throw an error when trying query a table with an unallowed default database', function (done) { 235 | _rethinkdb2['default'].connect({ port: proxyPort, db: 'someOtherDbNotAllowed' }).then(function (conn) { 236 | return _rethinkdb2['default'].table(tableName).run(conn); 237 | }).then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)).nodeify(done); 238 | }); 239 | 240 | it('should not throw an error when trying query a table with an unallowed table', function (done) { 241 | _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 242 | return _rethinkdb2['default'].table(tableName).coerceTo('array').run(conn); 243 | }).then(function (list) { 244 | return list.should.be['instanceof'](Array); 245 | }).nodeify(done); 246 | }); 247 | 248 | it('should throw an error when trying query a table with an unallowed default database', function (done) { 249 | _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }).then(function (conn) { 250 | return _rethinkdb2['default'].table('someOtherTable').run(conn); 251 | }).then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)).nodeify(done); 252 | }); 253 | }); 254 | 255 | describe('Query arguments', function () { 256 | 257 | it('should not allow a table name to be passed through r.args', function (done) { 258 | executeProxyQuery(_rethinkdb2['default'].db(dbName).table(_rethinkdb2['default'].args([tableName]))).then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)).nodeify(done); 259 | }); 260 | 261 | it('should allow a table name to be passed through r.expr', function (done) { 262 | executeProxyQuery(_rethinkdb2['default'].db(dbName).table(_rethinkdb2['default'].expr(tableName))).then(function (list) { 263 | list.should.be['instanceof'](Array); 264 | }).nodeify(done); 265 | }); 266 | 267 | it('should not allow a table name to be passed through a ReQL expression', function (done) { 268 | executeProxyQuery(_rethinkdb2['default'].db(dbName).table(_rethinkdb2['default'].db(dbName).tableList()(0))).then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)).nodeify(done); 269 | }); 270 | }); 271 | }); 272 | }); -------------------------------------------------------------------------------- /lib-test/edge-cases.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var _utils = require('./utils'); 23 | 24 | var _node_modulesRethinkdbProtoDef = require('../node_modules/rethinkdb/proto-def'); 25 | 26 | var _node_modulesRethinkdbProtoDef2 = _interopRequireDefault(_node_modulesRethinkdbProtoDef); 27 | 28 | var proxyPort = 8125; 29 | var dbName = 'rethinkdb_proxy_test'; 30 | var tableName = 'entries'; 31 | var server = undefined; 32 | 33 | var executeQuery = (0, _utils.makeExecuteQuery)(dbName, proxyPort); 34 | var executeProxyQuery = (0, _utils.makeExecuteProxyQuery)(dbName, proxyPort); 35 | var assertQuery = (0, _utils.makeAssertQuery)(executeQuery); 36 | var createDatabase = (0, _utils.makeCreateDatabase)(dbName, tableName); 37 | var dropDatabase = (0, _utils.makeDropDatabase)(dbName); 38 | var throwError = function throwError(res) { 39 | throw new Error(); 40 | }; 41 | var expectError = function expectError(errorName, errorMessageMatch, err) { 42 | if (errorName !== null) errorName.should.equal(err.name); 43 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 44 | (err instanceof Error).should.equal(true); 45 | }; 46 | 47 | describe('Edge Cases', function () { 48 | 49 | var get = _rethinkdb2['default'].db(dbName).table(tableName); 50 | 51 | describe('Replace', function () { 52 | 53 | before(function (done) { 54 | this.timeout(5000); 55 | createDatabase().then(function () { 56 | return new _bluebird2['default'](function (resolve, reject) { 57 | server = new _componentsRethinkdbProxy2['default']({ 58 | port: proxyPort, 59 | allowReplace: true 60 | }); 61 | server.listen(resolve); 62 | }); 63 | }).then(function () { 64 | return _rethinkdb2['default'].connect().then(function (conn) { 65 | return get.insert([{ 66 | id: 1, name: 'Hello' 67 | }, { 68 | id: 2, name: 'Jorge' 69 | }]).run(conn).then(function () { 70 | return conn.close(); 71 | }); 72 | }); 73 | }).nodeify(done); 74 | }); 75 | 76 | after(function (done) { 77 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 78 | }); 79 | 80 | it('should not allow a query that passes null to `replace` if `delete` is not allowed', function (done) { 81 | executeQuery(get.get(1).replace(null)).then(throwError, expectError.bind(null, 'ReqlDriverError', /REPLACE/i)).nodeify(done); 82 | }); 83 | 84 | it('should not allow a query that passes null to `replace` if `delete` is not allowed', function (done) { 85 | executeQuery(get.replace(null)).then(throwError, expectError.bind(null, 'ReqlDriverError', /REPLACE/i)).nodeify(done); 86 | }); 87 | }); 88 | 89 | describe('Insert', function () { 90 | 91 | before(function (done) { 92 | this.timeout(5000); 93 | createDatabase().then(function () { 94 | return new _bluebird2['default'](function (resolve, reject) { 95 | server = new _componentsRethinkdbProxy2['default']({ 96 | port: proxyPort, 97 | allowInsert: true 98 | }); 99 | server.listen(resolve); 100 | }); 101 | }).then(function () { 102 | return _rethinkdb2['default'].connect().then(function (conn) { 103 | return get.insert([{ 104 | id: 1, name: 'Hello' 105 | }, { 106 | id: 2, name: 'Jorge' 107 | }]).run(conn).then(function () { 108 | return conn.close(); 109 | }); 110 | }); 111 | }).nodeify(done); 112 | }); 113 | 114 | after(function (done) { 115 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 116 | }); 117 | 118 | it('should not allow a query that passes `conflict: replace` if `replace` is not allowed', function (done) { 119 | executeProxyQuery(get.insert({ id: 1, name: 'Hugo' }, { conflict: 'replace' })).then(throwError, expectError.bind(null, 'ReqlDriverError', /INSERT/i)).nodeify(done); 120 | }); 121 | 122 | it('should not allow a query that passes `conflict: update` if `update` is not allowed', function (done) { 123 | executeProxyQuery(get.insert({ id: 1, name: 'Hugo' }, { conflict: 'update' })).then(throwError, expectError.bind(null, 'ReqlDriverError', /UPDATE/i)).nodeify(done); 124 | }); 125 | }); 126 | }); -------------------------------------------------------------------------------- /lib-test/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 3 | 'use strict'; 4 | 5 | require('./parser.js'); 6 | 7 | require('./connection.js'); 8 | 9 | require('./queries.js'); 10 | 11 | require('./read-only-queries.js'); 12 | 13 | require('./edge-cases.js'); 14 | 15 | require('./database-and-table-access.js'); 16 | 17 | require('./changefeeds.js'); 18 | 19 | require('./parallel.js'); -------------------------------------------------------------------------------- /lib-test/parallel.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var _utils = require('./utils'); 23 | 24 | var _node_modulesRethinkdbProtoDef = require('../node_modules/rethinkdb/proto-def'); 25 | 26 | var _node_modulesRethinkdbProtoDef2 = _interopRequireDefault(_node_modulesRethinkdbProtoDef); 27 | 28 | var proxyPort = 8125; 29 | var dbName = 'rethinkdb_proxy_test'; 30 | var secondDbName = 'rethinkdb_proxy_test_2'; 31 | var tableName = 'entries'; 32 | var server = undefined; 33 | 34 | var executeQuery = (0, _utils.makeExecuteQuery)(dbName, proxyPort); 35 | var executeProxyQuery = (0, _utils.makeExecuteProxyQuery)(dbName, proxyPort); 36 | var assertQuery = (0, _utils.makeAssertQuery)(executeQuery); 37 | var createDatabase = (0, _utils.makeCreateDatabase)(dbName, tableName); 38 | var createSecondDatabase = (0, _utils.makeCreateDatabase)(secondDbName, tableName); 39 | var dropDatabase = (0, _utils.makeDropDatabase)(dbName); 40 | var throwError = function throwError(res) { 41 | throw new Error(); 42 | }; 43 | var expectError = function expectError(errorName, errorMessageMatch, err) { 44 | if (errorName !== null) errorName.should.equal(err.name); 45 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 46 | (err instanceof Error).should.equal(true); 47 | }; 48 | 49 | describe('Parallel Queries', function () { 50 | 51 | var testData = [{ name: 'Germany' }, { name: 'Bhutan' }, { name: 'Maldives' }]; 52 | 53 | before(function (done) { 54 | this.timeout(10000); 55 | createDatabase().then(function () { 56 | return _rethinkdb2['default'].connect().then(function (conn) { 57 | return _rethinkdb2['default'].db(dbName).table(tableName).insert(testData).run(conn); 58 | }); 59 | }).then(function () { 60 | return new _bluebird2['default'](function (resolve, reject) { 61 | server = new _componentsRethinkdbProxy2['default']({ 62 | port: proxyPort 63 | }); 64 | server.listen(resolve); 65 | }); 66 | }).nodeify(done); 67 | }); 68 | 69 | after(function (done) { 70 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 71 | }); 72 | 73 | it('should handle parallel connections', function (done) { 74 | //this.timeout(15000); 75 | var query = function query(countryName) { 76 | return _rethinkdb2['default'].db(dbName).table(tableName).filter({ 'name': countryName }).coerceTo('array'); 77 | }; 78 | var query1 = query('Germany'); 79 | var query2 = query('Maldives'); 80 | var arr = (function () { 81 | var arr = []; 82 | for (var i = 0; i < 5; i += 1) { 83 | arr.push(i); 84 | }return arr; 85 | })(); 86 | _rethinkdb2['default'].connect({ port: proxyPort }).then(function (conn1) { 87 | return _bluebird2['default'].all([].concat(arr.map(function () { 88 | return query1.run(conn1); 89 | })).concat(arr.map(function () { 90 | return query2.run(conn1); 91 | }))).then(function (res) { 92 | res.slice(0, 5).forEach(function (row) { 93 | return row[0].name.should.equal('Germany'); 94 | }); 95 | res.slice(5, 10).forEach(function (row) { 96 | return row[0].name.should.equal('Maldives'); 97 | }); 98 | }); 99 | }).nodeify(done); 100 | }); 101 | 102 | it('should handle parallel cursors', function (done) { 103 | _rethinkdb2['default'].connect({ port: proxyPort }).then(function (conn1) { 104 | return _rethinkdb2['default'].db(dbName).table(tableName).run(conn1).then(function (cursor) { 105 | var processRow = function processRow(err, row) { 106 | if (err === null) return cursor.next(processRow); 107 | if (err.msg === 'No more rows in the cursor.') return done(); 108 | return done(err); 109 | }; 110 | cursor.next(processRow); 111 | }); 112 | }); 113 | }); 114 | }); -------------------------------------------------------------------------------- /lib-test/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 4 | 5 | var _componentsBufferParser = require('./components/buffer-parser'); 6 | 7 | var _componentsBufferParser2 = _interopRequireDefault(_componentsBufferParser); 8 | 9 | var _node_modulesRethinkdbProtoDef = require('../node_modules/rethinkdb/proto-def'); 10 | 11 | var _node_modulesRethinkdbProtoDef2 = _interopRequireDefault(_node_modulesRethinkdbProtoDef); 12 | 13 | require('should'); 14 | 15 | describe('Buffer Parser', function () { 16 | 17 | var parser = undefined; 18 | var parseQuery = function parseQuery() { 19 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 20 | args[_key] = arguments[_key]; 21 | } 22 | 23 | var result = []; 24 | var cb = args.pop(); 25 | parser.on('query', function (query) { 26 | result.push(query); 27 | }); 28 | var count = 0; 29 | for (var key in args) { 30 | if (args.hasOwnProperty(key)) { 31 | var value = args[key]; 32 | var token = count++; 33 | var tokenBuffer = new Buffer(8); 34 | tokenBuffer.writeUInt32LE(Math.floor(token / 0xFFFFFFFF), 4); 35 | var byteLengthBuffer = new Buffer(4); 36 | var queryBuffer = new Buffer(JSON.stringify(value)); 37 | byteLengthBuffer.writeUInt32LE(queryBuffer.length, 0); 38 | parser.append(Buffer.concat([tokenBuffer, byteLengthBuffer, queryBuffer])); 39 | } 40 | } 41 | cb(result); 42 | }; 43 | 44 | beforeEach(function () { 45 | parser = new _componentsBufferParser2['default'](); 46 | var version = new Buffer(4); 47 | version.writeUInt32LE(_node_modulesRethinkdbProtoDef2['default'].VersionDummy.Version.VO_4, 0); 48 | var auth_buffer = new Buffer('', 'ascii'); 49 | var auth_length = new Buffer(4); 50 | auth_length.writeUInt32LE(auth_buffer.length, 0); 51 | var protocol = new Buffer(4); 52 | protocol.writeUInt32LE(_node_modulesRethinkdbProtoDef2['default'].VersionDummy.Protocol.JSON, 0); 53 | var token = Buffer.concat([version, auth_length, auth_buffer, protocol]); 54 | parser.append(token); 55 | }); 56 | 57 | it('should correctly parse a single buffer', function () { 58 | var value = [[1]]; 59 | parseQuery(value, function (result) { 60 | result[0].should.eql(value); 61 | }); 62 | }); 63 | 64 | it('should correctly parse a multiples buffers', function () { 65 | var value0 = [[1]]; 66 | var value1 = [1, [3, [4, [3]]]]; 67 | var value2 = [1, [3, [4, [3]]]]; 68 | parseQuery(value0, value1, value2, function (result) { 69 | result[0].should.eql(value0); 70 | result[1].should.eql(value1); 71 | result[2].should.eql(value2); 72 | }); 73 | }); 74 | 75 | it('should correctly parse a parser with `]` inside the bytelength/token`', function () { 76 | var value = [1, [51, [[39, [[15, [[14, ['rethinkdb_proxy_test']], 'entries']], { 'name': 'Germany' }]], 'array']]]; 77 | parseQuery(value, value, value, function (result) { 78 | result[0].should.eql(value); 79 | result[1].should.eql(value); 80 | result[2].should.eql(value); 81 | }); 82 | }); 83 | 84 | it('should correctly handle strings with `[]`', function () { 85 | var value = [1, [51, [[39, [[15, [[14, ['rethinkdb_proxy_test']], 'entries']], { 'name': '[[[[[[[' }]], 'array']]]; 86 | parseQuery(value, value, value, function (result) { 87 | result[0].should.eql(value); 88 | result[1].should.eql(value); 89 | result[2].should.eql(value); 90 | }); 91 | }); 92 | }); -------------------------------------------------------------------------------- /lib-test/queries.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var _utils = require('./utils'); 23 | 24 | var proxyPort = 8125; 25 | var dbName = 'rethinkdb_proxy_test'; 26 | var tableName = 'entries'; 27 | var server = undefined; 28 | 29 | var executeQuery = (0, _utils.makeExecuteQuery)(dbName, proxyPort); 30 | var assertQuery = (0, _utils.makeAssertQuery)(executeQuery); 31 | var createDatabase = (0, _utils.makeCreateDatabase)(dbName, tableName); 32 | var dropDatabase = (0, _utils.makeDropDatabase)(dbName); 33 | 34 | describe('Normal Queries', function () { 35 | 36 | before(function (done) { 37 | this.timeout(5000); 38 | createDatabase().then(function () { 39 | server = new _componentsRethinkdbProxy2['default']({ 40 | port: proxyPort, 41 | allowWrites: true 42 | }); 43 | server.listen(done); 44 | }); 45 | }); 46 | 47 | after(function (done) { 48 | server.close().then(dropDatabase).then(done.bind(null, null)); 49 | }); 50 | 51 | describe('Read Queries', function () { 52 | 53 | it('should return an `r.expr` successfully', function (done) { 54 | assertQuery(_rethinkdb2['default'].expr([1, 2, 3])).nodeify(done); 55 | }); 56 | 57 | it('should return the same list of databases', function (done) { 58 | assertQuery(_rethinkdb2['default'].dbList()).nodeify(done); 59 | }); 60 | 61 | it('should handle group queries', function (done) { 62 | assertQuery(_rethinkdb2['default'].expr([{ 'v': 1 }, { 'v': 2 }, { 'v': 2 }, { 'v': 4 }]).group('v').count().ungroup()).nodeify(done); 63 | }); 64 | }); 65 | 66 | describe('Write Queries', function () { 67 | 68 | it('should return the same result after a write', function (done) { 69 | var get = _rethinkdb2['default'].db(dbName).table(tableName); 70 | executeQuery(get.insert({ hello: 'world' })).then(function () { 71 | return assertQuery(get.orderBy('id')); 72 | }).then(function () { 73 | return _rethinkdb2['default'].connect().then(function (conn) { 74 | return get.count().run(conn).then(function (count) {}); 75 | }); 76 | }).nodeify(done); 77 | }); 78 | }); 79 | }); 80 | 81 | //count.should.eql(2); -------------------------------------------------------------------------------- /lib-test/read-only-queries.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | var _rethinkdb = require('rethinkdb'); 7 | 8 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 9 | 10 | var _bluebird = require('bluebird'); 11 | 12 | var _bluebird2 = _interopRequireDefault(_bluebird); 13 | 14 | var _componentsRethinkdbProxy = require('./components/rethinkdb-proxy'); 15 | 16 | var _componentsRethinkdbProxy2 = _interopRequireDefault(_componentsRethinkdbProxy); 17 | 18 | var _should = require('should'); 19 | 20 | var _should2 = _interopRequireDefault(_should); 21 | 22 | var _utils = require('./utils'); 23 | 24 | var _node_modulesRethinkdbProtoDef = require('../node_modules/rethinkdb/proto-def'); 25 | 26 | var _node_modulesRethinkdbProtoDef2 = _interopRequireDefault(_node_modulesRethinkdbProtoDef); 27 | 28 | var proxyPort = 8125; 29 | var dbName = 'rethinkdb_proxy_test'; 30 | var tableName = 'entries'; 31 | var server = undefined; 32 | 33 | var executeQuery = (0, _utils.makeExecuteQuery)(dbName, proxyPort); 34 | var executeProxyQuery = (0, _utils.makeExecuteProxyQuery)(dbName, proxyPort); 35 | var assertQuery = (0, _utils.makeAssertQuery)(executeQuery); 36 | var createDatabase = (0, _utils.makeCreateDatabase)(dbName, tableName); 37 | var dropDatabase = (0, _utils.makeDropDatabase)(dbName); 38 | var throwError = function throwError(res) { 39 | throw new Error(); 40 | }; 41 | var expectError = function expectError(errorName, errorMessageMatch, err) { 42 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 43 | if (errorName !== null) errorName.should.equal(err.name); 44 | (err instanceof Error).should.equal(true); 45 | }; 46 | 47 | describe('Unallowed Queries', function () { 48 | var get = _rethinkdb2['default'].db(dbName).table(tableName); 49 | 50 | before(function (done) { 51 | this.timeout(5000); 52 | createDatabase().then(function () { 53 | server = new _componentsRethinkdbProxy2['default']({ 54 | port: proxyPort 55 | }); 56 | return server.listen().then(done); 57 | }); 58 | }); 59 | 60 | after(function (done) { 61 | dropDatabase().then(server.close.bind(server)).then(done.bind(null, null)); 62 | }); 63 | 64 | describe('Read Queries', function () { 65 | 66 | it('should return an `r.expr` successfully', function (done) { 67 | assertQuery(_rethinkdb2['default'].expr([1, 2, 3])).nodeify(done); 68 | }); 69 | 70 | it('should return the same list of databases', function (done) { 71 | assertQuery(_rethinkdb2['default'].dbList()).nodeify(done); 72 | }); 73 | 74 | it('should allow for queries inside arrays', function (done) { 75 | assertQuery(_rethinkdb2['default'].expr([get.coerceTo('array'), get.coerceTo('array')])).nodeify(done); 76 | }); 77 | }); 78 | 79 | describe('Insert', function () { 80 | 81 | it('should throw an error after attempting to write to the database', function (done) { 82 | executeProxyQuery(get.insert({ hello: 'world' })).then(throwError, expectError.bind(null, 'ReqlDriverError', /INSERT/i)).nodeify(done); 83 | }); 84 | 85 | it('should not throw an error when using the same number as the `write` proto buff definition', function (done) { 86 | executeProxyQuery(_rethinkdb2['default'].expr([_node_modulesRethinkdbProtoDef2['default'].Term.TermType.INSERT, 1, 2, 3])).then(function (res) { 87 | res.should.eql([_node_modulesRethinkdbProtoDef2['default'].Term.TermType.INSERT, 1, 2, 3]); 88 | }).nodeify(done); 89 | }); 90 | 91 | it('should not allow insert queries inside `do`', function (done) { 92 | executeProxyQuery(_rethinkdb2['default'].expr([1, 2, 3])['do'](function (row) { 93 | return get.insert({ name: row }); 94 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /INSERT/i)).nodeify(done); 95 | }); 96 | 97 | it('should not allow for write queries inside arrays', function (done) { 98 | executeProxyQuery(_rethinkdb2['default'].expr([get['delete'](), get.coerceTo('array')])).then(throwError, expectError.bind(null, 'ReqlDriverError', /DELETE/i)).nodeify(done); 99 | }); 100 | }); 101 | 102 | describe('Delete', function () { 103 | it('should not allow delete queries inside `forEach` inside a `do`', function (done) { 104 | executeProxyQuery(get.coerceTo('array')['do'](function (rows) { 105 | return rows.forEach(function (row) { 106 | return get.get(row('id'))['delete'](); 107 | }); 108 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /DELETE/i)).nodeify(done); 109 | }); 110 | }); 111 | 112 | describe('HTTP', function () { 113 | 114 | it('should not allow http queries inside `map` inside a `do`', function (done) { 115 | executeProxyQuery(get.coerceTo('array').slice(0, 3)['do'](function (rows) { 116 | return rows.map(function (row) { 117 | return _rethinkdb2['default'].http('http://www.reddit.com/r/javascript.json'); 118 | }); 119 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /HTTP/i)).nodeify(done); 120 | }); 121 | 122 | it('should not allow http queries inside `do`', function (done) { 123 | executeProxyQuery(_rethinkdb2['default'].expr('hello')['do'](function (rows) { 124 | return _rethinkdb2['default'].http('http://www.reddit.com/r/javascript.json'); 125 | })).then(throwError, expectError.bind(null, 'ReqlDriverError', /HTTP/i)).nodeify(done); 126 | }); 127 | }); 128 | }); -------------------------------------------------------------------------------- /lib-test/utils.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 5 | 6 | Object.defineProperty(exports, '__esModule', { 7 | value: true 8 | }); 9 | exports.makeExecuteQuery = makeExecuteQuery; 10 | exports.makeExecuteProxyQuery = makeExecuteProxyQuery; 11 | exports.makeAssertQuery = makeAssertQuery; 12 | exports.makeCreateDatabase = makeCreateDatabase; 13 | exports.makeDropDatabase = makeDropDatabase; 14 | 15 | var _bluebird = require('bluebird'); 16 | 17 | var _bluebird2 = _interopRequireDefault(_bluebird); 18 | 19 | var _rethinkdb = require('rethinkdb'); 20 | 21 | var _rethinkdb2 = _interopRequireDefault(_rethinkdb); 22 | 23 | function makeExecuteQuery(dbName, proxyPort) { 24 | return function (query) { 25 | return _bluebird2['default'].resolve().then(function () { 26 | return [_rethinkdb2['default'].connect({ db: dbName }), _rethinkdb2['default'].connect({ port: proxyPort, db: dbName })]; 27 | }).spread(function (connA, connB) { 28 | return _bluebird2['default'].all([query.run(connA), query.run(connB)]).spread(function (resultA, resultB) { 29 | if (typeof resultA.toArray === 'function' && typeof resultA.each === 'function') { 30 | return _bluebird2['default'].all([resultA.toArray(), resultB.toArray()]); 31 | } 32 | return [resultA, resultB]; 33 | })['finally'](function () { 34 | return _bluebird2['default'].all([connA.close(), connB.close()]); 35 | }); 36 | }); 37 | }; 38 | } 39 | 40 | function makeExecuteProxyQuery(dbName, proxyPort) { 41 | return function (query) { 42 | return _bluebird2['default'].resolve().then(function () { 43 | return _rethinkdb2['default'].connect({ port: proxyPort, db: dbName }); 44 | }).then(function (conn) { 45 | return query.run(conn).then(function (result) { 46 | if (typeof result.toArray === 'function' && typeof result.each === 'function') { 47 | return result.toArray(); 48 | } 49 | return result; 50 | })['finally'](function () { 51 | return conn.close(); 52 | }); 53 | }); 54 | }; 55 | } 56 | 57 | function makeAssertQuery(executeQuery) { 58 | return function (query) { 59 | return executeQuery(query).spread(function (resultA, resultB) { 60 | return resultA.should.eql(resultB); 61 | }); 62 | }; 63 | } 64 | 65 | function makeCreateDatabase(dbName, tableName) { 66 | return function (done) { 67 | return _rethinkdb2['default'].connect().then(function (conn) { 68 | return _bluebird2['default'].resolve().then(function () { 69 | return _rethinkdb2['default'].dbCreate(dbName).run(conn)['catch'](function (err) {}); 70 | }).then(function () { 71 | return _rethinkdb2['default'].db(dbName).tableCreate(tableName).run(conn)['catch'](function (err) {}); 72 | }); 73 | }); 74 | }; 75 | } 76 | 77 | function makeDropDatabase(dbName) { 78 | return function () { 79 | return _rethinkdb2['default'].connect().then(function (conn) { 80 | return _rethinkdb2['default'].dbDrop(dbName).run(conn)['finally'](function () { 81 | return conn.close(); 82 | }); 83 | }); 84 | }; 85 | } -------------------------------------------------------------------------------- /lib/buffer-parser.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 'use strict'; 3 | 4 | var _createClass = require('babel-runtime/helpers/create-class')['default']; 5 | 6 | var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default']; 7 | 8 | var _Symbol = require('babel-runtime/core-js/symbol')['default']; 9 | 10 | Object.defineProperty(exports, '__esModule', { 11 | value: true 12 | }); 13 | var _listeners_ = _Symbol('listeners'); 14 | var _queue_ = _Symbol('queue'); 15 | 16 | var BufferParser = (function () { 17 | function BufferParser() { 18 | _classCallCheck(this, BufferParser); 19 | 20 | this[_queue_] = new Buffer(0); 21 | this[_listeners_] = {}; 22 | this.protoVersion = null; 23 | this.authKeyLength = null; 24 | this.authKey = null; 25 | this.protoProtocol = null; 26 | } 27 | 28 | _createClass(BufferParser, [{ 29 | key: 'append', 30 | value: function append(buff) { 31 | if (buff !== undefined) { 32 | this[_queue_] = Buffer.concat([this[_queue_], buff]); 33 | } 34 | if (this.protoVersion === null) { 35 | if (Buffer.byteLength(this[_queue_]) >= 4) { 36 | this.protoVersion = this[_queue_].slice(0, 4).readUInt32LE(); 37 | this[_queue_] = this[_queue_].slice(4); 38 | // Call code again to ensure we don't have the authKey 39 | this.append(); 40 | } 41 | } else if (this.authKeyLength === null) { 42 | if (Buffer.byteLength(this[_queue_]) >= 4) { 43 | this.authKeyLength = this[_queue_].slice(0, 4).readUInt32LE(); 44 | this[_queue_] = this[_queue_].slice(4); 45 | // Call code again to ensure we don't have the authKey 46 | this.append(); 47 | } 48 | } else if (this.authKey === null) { 49 | if (Buffer.byteLength(this[_queue_] >= this.authKeyLength)) { 50 | this.authKey = this[_queue_].slice(0, this.authKeyLength).toString('ascii'); 51 | this[_queue_] = this[_queue_].slice(this.authKeyLength); 52 | // Call code again to ensure we don't have the authKey 53 | this.append(); 54 | } 55 | } else if (this.protoProtocol === null) { 56 | if (Buffer.byteLength(this[_queue_]) >= 4) { 57 | this.protoProtocol = this[_queue_].slice(0, 4).readUInt32LE(); 58 | this[_queue_] = this[_queue_].slice(4); 59 | // Call code again to ensure we don't have the authKey 60 | this.append(); 61 | this.fireEvent('connect', this.protoVersion, this.authKey, this.protoProtocol); 62 | } 63 | } else { 64 | this.parseQuery(); 65 | } 66 | } 67 | }, { 68 | key: 'fireEvent', 69 | value: function fireEvent(eventName) { 70 | for (var _len = arguments.length, data = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 71 | data[_key - 1] = arguments[_key]; 72 | } 73 | 74 | if (this[_listeners_][eventName] !== undefined) { 75 | this[_listeners_][eventName].forEach(function (func) { 76 | func.apply(null, data); 77 | }); 78 | } 79 | } 80 | }, { 81 | key: 'parseQuery', 82 | value: function parseQuery() { 83 | /*! 84 | * Assumptions 85 | * 1. All clients have their own instance of BufferParser 86 | * 2. TCP connections send messages in order 87 | * 3. Because messages are sent in order by the client through a TCP connection, 88 | * we can assume that all packets are tokens followed by query bytelenghts 89 | * followed by queries 90 | */ 91 | var newQueue = null; 92 | var _prevQuery = null; 93 | /*! 94 | * Make sure we have all three components necessary to parse the query: 95 | * token, byteLength, and query 96 | */ 97 | if (Buffer.byteLength(this[_queue_]) <= 12) return; 98 | var token = this[_queue_].slice(0, 8).readUInt32LE(); 99 | var byteLength = this[_queue_].slice(8, 12).readUInt32LE(); 100 | var query = this[_queue_].slice(12, byteLength + 12); 101 | newQueue = this[_queue_].slice(byteLength + 12); 102 | try { 103 | // Simplest way to handle check if input is valid JSON 104 | var json = JSON.parse(query.toString()); 105 | this.fireEvent('query', json, token); 106 | } catch (err) { 107 | // I think the problem has something to do with the fact that I'm adding a 108 | // comma somehwere .... 109 | this.fireEvent('error', token); 110 | } 111 | this[_queue_] = new Buffer(0); 112 | if (newQueue !== null && newQueue.length > 0 && Buffer.byteLength(newQueue) >= 8) { 113 | var _token = newQueue.slice(0, 8).readUInt32LE(); 114 | if (parseInt(_token) === +_token) { 115 | this.append(newQueue); 116 | } 117 | } 118 | } 119 | }, { 120 | key: 'on', 121 | value: function on(eventName, func) { 122 | if (this[_listeners_][eventName] === undefined) { 123 | this[_listeners_][eventName] = []; 124 | } 125 | this[_listeners_][eventName].push(func); 126 | } 127 | }]); 128 | 129 | return BufferParser; 130 | })(); 131 | 132 | exports['default'] = BufferParser; 133 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true, bitwise:false */ 2 | 'use strict'; 3 | 4 | var _createClass = require('babel-runtime/helpers/create-class')['default']; 5 | 6 | var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default']; 7 | 8 | var _Promise = require('babel-runtime/core-js/promise')['default']; 9 | 10 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 11 | 12 | Object.defineProperty(exports, '__esModule', { 13 | value: true 14 | }); 15 | 16 | require('babel/polyfill'); 17 | 18 | var _net = require('net'); 19 | 20 | var _net2 = _interopRequireDefault(_net); 21 | 22 | var _rethinkdbProtoDef = require('rethinkdb/proto-def'); 23 | 24 | var _rethinkdbProtoDef2 = _interopRequireDefault(_rethinkdbProtoDef); 25 | 26 | var _bufferParser = require('./buffer-parser'); 27 | 28 | var _bufferParser2 = _interopRequireDefault(_bufferParser); 29 | 30 | var _queryParser = require('./query-parser'); 31 | 32 | var _optionsParser = require('./options-parser'); 33 | 34 | var _optionsParser2 = _interopRequireDefault(_optionsParser); 35 | 36 | var _loggerJs = require('./logger.js'); 37 | 38 | var _loggerJs2 = _interopRequireDefault(_loggerJs); 39 | 40 | var RethinkDBProxy = (function () { 41 | function RethinkDBProxy(opts) { 42 | _classCallCheck(this, RethinkDBProxy); 43 | 44 | // Set defaults and unallowedTerms 45 | this.opts = (0, _optionsParser2['default'])(opts); 46 | this.logger = new _loggerJs2['default'](opts.inLevel, opts.outLevel, opts.sysLevel); 47 | this.__connections = []; 48 | this.server = _net2['default'].createServer(this.connectionHandler.bind(this)); 49 | return this; 50 | } 51 | 52 | _createClass(RethinkDBProxy, [{ 53 | key: 'listen', 54 | value: function listen(cb) { 55 | var _this = this; 56 | 57 | return new _Promise(function (resolve, reject) { 58 | _this.server.listen(_this.opts.port, function (err) { 59 | if (err) { 60 | _this.logger.sys.info({ err: err, port: _this.opts.port }, 'Error tryingg to listen for traffic'); 61 | if (typeof cb === 'function') cb(err); 62 | reject(err); 63 | } 64 | _this.logger.sys.info({ port: _this.opts.port }, 'server.listen'); 65 | if (typeof cb === 'function') cb(); 66 | resolve(); 67 | }); 68 | }); 69 | } 70 | }, { 71 | key: 'close', 72 | value: function close(cb) { 73 | var _this2 = this; 74 | 75 | return new _Promise(function (resolve, reject) { 76 | _this2.server.close(function () { 77 | _this2.logger.sys.info('server.close'); 78 | if (typeof cb === 'function') cb(); 79 | resolve(); 80 | }); 81 | setTimeout(function () { 82 | _this2.logger.sys.info('Server closing all connections'); 83 | _this2.__connections.forEach(function (conn) { 84 | _this2.logger.sys.trace({ conn: conn }, 'conn.destroy'); 85 | conn.destroy(); 86 | }); 87 | }, 100); 88 | }); 89 | } 90 | }, { 91 | key: 'makeSendResponse', 92 | value: function makeSendResponse(socket) { 93 | var _this3 = this; 94 | 95 | return function (response, token) { 96 | _this3.logger.out.info({ 97 | token: token, 98 | response: response 99 | }, 'Send Response'); 100 | var tokenBuffer = new Buffer(8); 101 | tokenBuffer.writeUInt32LE(token & 0xFFFFFFFF, 0); 102 | tokenBuffer.writeUInt32LE(Math.floor(token / 0xFFFFFFFF), 4); 103 | var responseBuffer = new Buffer(JSON.stringify(response)); 104 | var lengthBuffer = new Buffer(4); 105 | lengthBuffer.writeUInt32LE(responseBuffer.length, 0); 106 | if (socket.destroyed) return; 107 | socket.write(tokenBuffer); 108 | socket.write(lengthBuffer); 109 | socket.write(responseBuffer); 110 | }; 111 | } 112 | }, { 113 | key: 'makeSendError', 114 | value: function makeSendError(clientSocket) { 115 | var _this4 = this; 116 | 117 | var sendResponseToClient = this.makeSendResponse(clientSocket); 118 | return function (token, message) { 119 | var response = { 120 | t: _rethinkdbProtoDef2['default'].Response.ResponseType.CLIENT_ERROR, 121 | b: [], 122 | n: [], 123 | r: [message] 124 | }; 125 | _this4.logger.out.info({ 126 | message: message, 127 | response: response, 128 | token: token 129 | }, 'Send Error'); 130 | sendResponseToClient(response, token); 131 | }; 132 | } 133 | }, { 134 | key: 'connectionHandler', 135 | value: function connectionHandler(clientSocket) { 136 | var _this5 = this; 137 | 138 | this.__connections.push(clientSocket); 139 | 140 | clientSocket.connected = false; 141 | var parser = new _bufferParser2['default'](); 142 | var serverSocket = _net2['default'].connect(this.opts.rdbPort, this.opts.rdbHost); 143 | var sendResponseToServer = this.makeSendResponse(serverSocket); 144 | var sendResponseToClient = this.makeSendResponse(clientSocket); 145 | var sendError = this.makeSendError(clientSocket); 146 | 147 | /*! 148 | * Listeners 149 | */ 150 | serverSocket.on('data', function (buff) { 151 | _this5.logger.out.info({ 152 | buffer: buff.toString() 153 | }, 'Data received'); 154 | if (buff.toString() === 'SUCCESS') clientSocket.connected = true; 155 | // NOTE: The socket might try to write something even if the connection 156 | // is closed 157 | if (clientSocket.destroyed) return; 158 | clientSocket.write(buff); 159 | }); 160 | 161 | parser.on('connect', function (version, authKey, protoProtocol) { 162 | _this5.logger['in'].info({ 163 | version: version, 164 | authKey: authKey, 165 | protoProtocol: protoProtocol 166 | }, 'parser.on.connect'); 167 | var versionBuffer = new Buffer(4); 168 | versionBuffer.writeUInt32LE(version, 0); 169 | var authBuffer = new Buffer(authKey, 'ascii'); 170 | var authLengthBuffer = new Buffer(4); 171 | authLengthBuffer.writeUInt32LE(authBuffer.length, 0); 172 | var protocolBuffer = new Buffer(4); 173 | protocolBuffer.writeUInt32LE(protoProtocol, 0); 174 | var token = Buffer.concat([versionBuffer, authLengthBuffer, authBuffer, protocolBuffer]); 175 | if (protoProtocol !== _rethinkdbProtoDef2['default'].VersionDummy.Protocol.JSON) { 176 | sendError(token, 'Proxy Error: Only JSON protocol allowed.'); 177 | } 178 | serverSocket.write(token); 179 | }); 180 | 181 | parser.on('error', function (token) { 182 | _this5.logger['in'].error({ 183 | token: token 184 | }, 'parser.on.error'); 185 | sendError('Proxy Error: Could not parse query correctly.'); 186 | }); 187 | 188 | parser.on('query', function (query, token) { 189 | _this5.logger['in'].info({ 190 | query: query, 191 | token: token 192 | }, 'parser.on.query'); 193 | var termsFound = (0, _queryParser.findTermsOrErrors)(_this5.opts, _this5.opts.unallowedTerms, query); 194 | if (termsFound.length > 0) { 195 | // This shouldn't throw an error. It should 196 | // send the error through the TCP connection 197 | var errorMessage = undefined; 198 | if (typeof termsFound[0] === 'object' && typeof termsFound[0].error === 'string') { 199 | errorMessage = termsFound[0].error; 200 | } else { 201 | errorMessage = 'Cannot execute query. "' + termsFound + '" query not allowed.'; 202 | } 203 | _this5.logger['in'].warn({ 204 | termsFound: termsFound, 205 | errorMessage: errorMessage 206 | }, 'parser.on.query Error'); 207 | sendError(token, errorMessage); 208 | } 209 | return sendResponseToServer(query, token); 210 | }); 211 | 212 | clientSocket.on('data', function (data) { 213 | _this5.logger['in'].trace({ 214 | data: data 215 | }, 'clientSocket.on.data'); 216 | return parser.append(data); 217 | }); 218 | } 219 | }]); 220 | 221 | return RethinkDBProxy; 222 | })(); 223 | 224 | exports['default'] = RethinkDBProxy; 225 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = require('babel-runtime/helpers/create-class')['default']; 4 | 5 | var _classCallCheck = require('babel-runtime/helpers/class-call-check')['default']; 6 | 7 | var _Symbol = require('babel-runtime/core-js/symbol')['default']; 8 | 9 | var _Object$keys = require('babel-runtime/core-js/object/keys')['default']; 10 | 11 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 12 | 13 | Object.defineProperty(exports, '__esModule', { 14 | value: true 15 | }); 16 | 17 | var _bunyan = require('bunyan'); 18 | 19 | var _bunyan2 = _interopRequireDefault(_bunyan); 20 | 21 | var _log_ = _Symbol('log'); 22 | 23 | var levels = { 24 | 10: 'trace', 25 | 20: 'debug', 26 | 30: 'info', 27 | 40: 'warn', 28 | 50: 'error', 29 | 60: 'fatal' 30 | }; 31 | 32 | exports.levels = levels; 33 | 34 | var LevelLogger = (function () { 35 | function LevelLogger(globalLevel, logger) { 36 | var _this = this; 37 | 38 | _classCallCheck(this, LevelLogger); 39 | 40 | this[_log_] = logger; 41 | // "fatal" (60): The service/app is going to stop or become unusable now. 42 | // An operator should definitely look into this soon. 43 | // "error" (50): Fatal for a particular request, but the service/app continues servicing other requests. 44 | // An operator should look at this soon(ish). 45 | // "warn" (40): A note on something that should probably be looked at by an operator eventually. 46 | // "info" (30): Detail on regular operation. 47 | // "debug" (20): Anything else, i.e. too verbose to be included in "info" level. 48 | // "trace" (10): Logging from external libraries used by your app or very detailed application logging. 49 | _Object$keys(levels).forEach(function (levelKey) { 50 | _this[levelKey] = _this.log(globalLevel, levelKey, levels[levelKey]); 51 | _this[levels[levelKey]] = _this.log(globalLevel, levelKey, levels[levelKey]); 52 | }); 53 | } 54 | 55 | _createClass(LevelLogger, [{ 56 | key: 'log', 57 | value: function log(globalLevel, level, levelName) { 58 | var _this2 = this; 59 | 60 | return function () { 61 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 62 | args[_key] = arguments[_key]; 63 | } 64 | 65 | _this2[_log_][levelName].apply(_this2[_log_], args); 66 | }; 67 | } 68 | }]); 69 | 70 | return LevelLogger; 71 | })(); 72 | 73 | var Logger = function Logger(inLevel, outLevel, sysLevel) { 74 | _classCallCheck(this, Logger); 75 | 76 | var logger = _bunyan2['default'].createLogger({ name: 'rethinkdb-proxy' }); 77 | this['in'] = new LevelLogger(inLevel, logger); 78 | this.out = new LevelLogger(outLevel, logger); 79 | this.sys = new LevelLogger(sysLevel, logger); 80 | }; 81 | 82 | exports['default'] = Logger; -------------------------------------------------------------------------------- /lib/options-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _Object$keys = require('babel-runtime/core-js/object/keys')['default']; 4 | 5 | var _Object$assign = require('babel-runtime/core-js/object/assign')['default']; 6 | 7 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 8 | 9 | Object.defineProperty(exports, '__esModule', { 10 | value: true 11 | }); 12 | 13 | var _taser = require('taser'); 14 | 15 | var _taser2 = _interopRequireDefault(_taser); 16 | 17 | var _loggerJs = require('./logger.js'); 18 | 19 | var assertStringOrArray = (0, _taser2['default'])(['string', 'array']); 20 | var assertBoolean = (0, _taser2['default'])(['boolean']); 21 | var assertNumber = (0, _taser2['default'])(['number']); 22 | var assertString = (0, _taser2['default'])(['string']); 23 | var assertLevelString = (0, _taser2['default'])({ 24 | type: 'string', 25 | values: (function () { 26 | return _Object$keys(_loggerJs.levels).map(function (levelKey) { 27 | return _loggerJs.levels[levelKey]; 28 | }); 29 | })() 30 | }); 31 | 32 | var getAllowedTermsOpts = function getAllowedTermsOpts(opts) { 33 | // By default, don't allow any of these terms 34 | opts.unallowedTerms = ['INSERT', 'UPDATE', 'REPLACE', 'DELETE', 'DB_CREATE', 'DB_DROP', 'TABLE_CREATE', 'TABLE_DROP', 'INDEX_CREATE', 'INDEX_DROP', 'INDEX_RENAME', 'RECONFIGURE', 'REBALANCE', 'HTTP', 'JAVASCRIPT']; 35 | 36 | var toUpperCaseSnakeCase = function toUpperCaseSnakeCase(str) { 37 | return str.replace(/(^[A-Z])/g, function ($1) { 38 | return $1.toLowerCase(); 39 | }).replace(/([A-Z])/g, function ($1) { 40 | return '_' + $1.toUpperCase(); 41 | }).toUpperCase(); 42 | }; 43 | var allowTerm = function allowTerm(termName) { 44 | if (opts.unallowedTerms.includes(termName)) { 45 | opts.unallowedTerms.splice(opts.unallowedTerms.indexOf(termName), 1); 46 | } 47 | }; 48 | // Allow all terms specified by user (single terms) 49 | for (var key in opts) { 50 | if (opts.hasOwnProperty(key)) { 51 | if (opts[key] === true && key.substring(0, 5) === 'allow') { 52 | var termName = toUpperCaseSnakeCase(key.substring(5)); 53 | allowTerm(termName); 54 | } 55 | } 56 | } 57 | // Allow all terms specified (multiple terms) 58 | if (opts.allowWrites) { 59 | allowTerm('INSERT'); 60 | allowTerm('UPDATE'); 61 | allowTerm('DELETE'); 62 | } 63 | if (opts.allowIndexes) { 64 | allowTerm('INDEX_CREATE'); 65 | allowTerm('INDEX_DROP'); 66 | allowTerm('INDEX_RENAME'); 67 | } 68 | return opts; 69 | }; 70 | 71 | var getAllowedDatabaseOpts = function getAllowedDatabaseOpts(opts) { 72 | // Clean options 73 | if (typeof opts.dbs === 'string') opts.dbs = [opts.dbs]; 74 | if (typeof opts.tables === 'string') opts.tables = [opts.tables]; 75 | 76 | // Create object for dbs 77 | var databases = opts.dbs; 78 | opts.dbs = { 79 | $$count: 0 // `$` not allowed in RethinkDB database names 80 | }; 81 | databases.forEach(function (database) { 82 | opts.dbs[database] = { allowed: true, tables: {}, $$count: 0 }; 83 | // `$` not allowed in RethinkDB database names 84 | opts.dbs.$$count += 1; 85 | }); 86 | opts.tables.forEach(function (tableName) { 87 | if (opts.dbs.$$count > 1) { 88 | var split = tableName.split('.'); 89 | if (split.length !== 2) { 90 | var message = 'If more than 1 database is passed, '; 91 | message += 'all table names must be dot (`.`) separated with the table names.'; 92 | message += ' `' + tableName + '` is not valid.'; 93 | throw new Error(message); 94 | } 95 | if (opts.dbs[split[0]] === undefined) { 96 | var message = 'Database ' + split[0] + ' in ' + tableName + ' was not declared'; 97 | throw new Error(message); 98 | } 99 | opts.dbs[split[0]].tables[split[1]] = { allowed: true }; 100 | opts.dbs[split[0]].$$count += 1; 101 | } 102 | }); 103 | return opts; 104 | }; 105 | 106 | var parseLoggingOpts = function parseLoggingOpts(opts) { 107 | // Set default levels 108 | if (opts.logLevel === undefined) opts.logLevel = 'fatal'; 109 | if (opts.logLevelIn === undefined) opts.logLevelIn = opts.logLevel; 110 | if (opts.logLevelOut === undefined) opts.logLevelOut = opts.logLevel; 111 | if (opts.logLevelSys === undefined) opts.logLevelSys = opts.logLevel; 112 | var levelsStringToInts = _Object$keys(_loggerJs.levels).reduce(function (keysObj, key) { 113 | var obj = {}; 114 | obj[_loggerJs.levels[key]] = key; 115 | return _Object$assign(keysObj, obj); 116 | }, {}); 117 | opts.logLevel = levelsStringToInts[opts.logLevel]; 118 | opts.logLevelIn = levelsStringToInts[opts.logLevelIn]; 119 | opts.logLevelOut = levelsStringToInts[opts.logLevelOut]; 120 | opts.logLevelSys = levelsStringToInts[opts.logLevelSys]; 121 | return opts; 122 | }; 123 | 124 | exports['default'] = function (opts) { 125 | // Define Options and defaults 126 | opts = _Object$assign({ 127 | port: 8125, 128 | rdbHost: 'localhost', 129 | rdbPort: 28015, 130 | logLevel: 'fatal', 131 | logLevelIn: 'fatal', 132 | logLevelOut: 'fatal', 133 | logLevelSys: 'fatal', 134 | dbs: [], 135 | tables: [], 136 | allowSysDbAccess: false, 137 | allowWrites: false, // Allow insert, update, delete 138 | allowInsert: false, 139 | allowUpdate: false, 140 | allowReplace: false, 141 | allowDelete: false, 142 | allowDbCreate: false, 143 | allowDbDrop: false, 144 | allowTableCreate: false, 145 | allowTableDrop: false, 146 | allowIndexes: false, // Allow indexCreate, indexDrop, indexRename 147 | allowIndexCreate: false, 148 | allowIndexDrop: false, 149 | allowIndexRename: false, 150 | allowReconfigure: false, 151 | allowRebalance: false, 152 | allowHttp: false, 153 | allowJavascript: false 154 | }, opts); 155 | 156 | // Ensure validity of inputs 157 | assertNumber(opts.port); 158 | assertNumber(opts.rdbPort); 159 | assertString(opts.rdbHost); 160 | assertLevelString(opts.logLevel); 161 | assertLevelString(opts.logLevelIn); 162 | assertLevelString(opts.logLevelOut); 163 | assertLevelString(opts.logLevelSys); 164 | assertStringOrArray(opts.dbs); 165 | assertStringOrArray(opts.tables); 166 | 167 | // Assert booleans 168 | for (var key in opts) { 169 | if (opts.hasOwnProperty(key) && key.substring(0, 5) === 'allow') { 170 | assertBoolean(opts[key]); 171 | } 172 | } 173 | 174 | // Parse logging options 175 | opts = parseLoggingOpts(opts); 176 | 177 | // Get allowed databases 178 | opts = getAllowedDatabaseOpts(opts); 179 | 180 | // Get allowed terms 181 | opts = getAllowedTermsOpts(opts); 182 | 183 | return opts; 184 | }; 185 | 186 | module.exports = exports['default']; -------------------------------------------------------------------------------- /lib/query-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _Number$isInteger = require('babel-runtime/core-js/number/is-integer')['default']; 4 | 5 | var _interopRequireDefault = require('babel-runtime/helpers/interop-require-default')['default']; 6 | 7 | Object.defineProperty(exports, '__esModule', { 8 | value: true 9 | }); 10 | 11 | var _lodash = require('lodash'); 12 | 13 | var _lodash2 = _interopRequireDefault(_lodash); 14 | 15 | var _rethinkdbProtoDef = require('rethinkdb/proto-def'); 16 | 17 | var _rethinkdbProtoDef2 = _interopRequireDefault(_rethinkdbProtoDef); 18 | 19 | var isRQLQuery = function isRQLQuery(query) { 20 | // Duck typing a query... 21 | if (!Array.isArray(query)) return false; 22 | if (query.length < 2 || query.length > 3) return false; 23 | if (!_Number$isInteger(query[0])) return false; 24 | if (query[2] !== undefined && typeof query[2] !== 'object' && !Array.isArray(query[2])) { 25 | return false; 26 | } 27 | return true; 28 | }; 29 | 30 | var checkForDeleteInReplace = function checkForDeleteInReplace(terms, command, args, query_opts) { 31 | if (command === _rethinkdbProtoDef2['default'].Term.TermType.REPLACE && !terms.includes('REPLACE') && terms.includes('DELETE')) { 32 | var argPassedToReplace = args[args.length - 1]; 33 | if (argPassedToReplace === null) { 34 | return [{ 35 | 'error': 'Using the `REPLACE` term with `null` is not allowed if `DELETE` is not also allowed.' 36 | }]; 37 | } 38 | } 39 | return []; 40 | }; 41 | 42 | var checkForUpdateInInsert = function checkForUpdateInInsert(terms, command, args, query_opts) { 43 | if (command === _rethinkdbProtoDef2['default'].Term.TermType.INSERT && !terms.includes('INSERT') && terms.includes('UPDATE')) { 44 | if (typeof query_opts === 'object' && typeof query_opts.conflict === 'string' && query_opts.conflict.toLowerCase() === 'update') { 45 | return [{ 46 | 'error': 'Using the `INSERT` term with `conflict: update` is not allowed if `UPDATE` is not also allowed.' 47 | }]; 48 | } 49 | } 50 | return []; 51 | }; 52 | 53 | var checkForDeleteInInsert = function checkForDeleteInInsert(terms, command, args, query_opts) { 54 | if (command === _rethinkdbProtoDef2['default'].Term.TermType.INSERT && !terms.includes('INSERT') && terms.includes('REPLACE')) { 55 | if (typeof query_opts === 'object' && typeof query_opts.conflict === 'string' && query_opts.conflict.toLowerCase() === 'replace') { 56 | return [{ 57 | 'error': 'Using the `INSERT` term with `conflict: replace` is not allowed if `REPLACE` is not also allowed.' 58 | }]; 59 | } 60 | } 61 | return []; 62 | }; 63 | 64 | var checkForTableAccess = function checkForTableAccess(opts, connectionDbName, command, args, query_opts) { 65 | if (command === _rethinkdbProtoDef2['default'].Term.TermType.TABLE && typeof opts === 'object') { 66 | var tableName = args[args.length - 1]; 67 | var dbName = connectionDbName; 68 | /*! 69 | * Since the name of a table can be passed dynamically and this introducues 70 | * a lot of complexity to determining whether a tables is allowed, only 71 | * allow strings, which cover most cases 72 | */ 73 | if (typeof tableName !== 'string') { 74 | return [{ 75 | 'error': 'Only strings are allowed for table names while using the `table` command' 76 | }]; 77 | } 78 | if (opts.dbs.$$count > 0 && (typeof opts.dbs[dbName] === 'object' && opts.dbs[dbName].allowed)) { 79 | if (opts.dbs[dbName].$$count > 0 && (typeof opts.dbs[dbName].tables[tableName] !== 'object' || !opts.dbs[dbName].tables[tableName].allowed)) { 80 | return [{ 'error': 'Access to the `{$tableName}` table is not allowed.' + ' Table must be declared in the `tables` and' + ' database must be inlcluded in `dbs` parameter' 81 | }]; 82 | } 83 | } 84 | } 85 | return []; 86 | }; 87 | 88 | var checkForDatabaseAccess = function checkForDatabaseAccess(opts, connectionDbName, command, args, query_opts) { 89 | if (command === _rethinkdbProtoDef2['default'].Term.TermType.DB && typeof opts === 'object') { 90 | var dbName = args[args.length - 1]; 91 | /*! 92 | * Since the name of a database can be passed dynamically and this introduces 93 | * a lot of complexity to determining whether a tables is allowed, only 94 | * allow strings, which cover most cases 95 | */ 96 | if (typeof dbName !== 'string') { 97 | return [{ 98 | 'error': 'Only strings are allowed for database names while using the `db` command' 99 | }]; 100 | } 101 | if (!opts.allowSysDbAccess && dbName === 'rethinkdb') { 102 | return [{ 103 | 'error': 'Access to the `rethinkdb` database is not allowed unless explicitly stated with `allowSysDbAccess`' 104 | }]; 105 | } 106 | if (opts.dbs.$$count > 0 && (typeof opts.dbs[dbName] !== 'object' || !opts.dbs[dbName].allowed)) { 107 | return [{ 'error': 'Access to the `' + dbName + '` database is not allowed. ' + 'Database must be inlcluded in `db` parameter' }]; 108 | } 109 | } 110 | return []; 111 | }; 112 | 113 | var findTermsOrErrors = function findTermsOrErrors(opts, terms, query) { 114 | var termsFound = [], 115 | connectionDbName = undefined; 116 | 117 | var __findTermsOrErrors = function __findTermsOrErrors(query) { 118 | if (!isRQLQuery(query)) { 119 | if (Array.isArray(query)) { 120 | return _lodash2['default'].flatten(query.map(__findTermsOrErrors)).filter(function (x) { 121 | return x; 122 | }); 123 | } 124 | return []; 125 | } 126 | var command = query[0], 127 | args = query[1], 128 | query_opts = query[2]; 129 | 130 | /*! 131 | * Edge cases 132 | */ 133 | var errorsFound = [] 134 | // #1 If `replace` is allowed but `delete` is not 135 | .concat(checkForDeleteInReplace(terms, command, args, query_opts)) 136 | // #2 If `insert` is allowed but `update` is not 137 | .concat(checkForUpdateInInsert(terms, command, args, query_opts)) 138 | // #3 If `insert` is allowed but `delete` is not 139 | .concat(checkForDeleteInInsert(terms, command, args, query_opts)); 140 | if (errorsFound.length > 0) return errorsFound; 141 | 142 | /*! 143 | * Database and table access 144 | */ 145 | errorsFound = [].concat(checkForDatabaseAccess(opts, connectionDbName, command, args, query_opts)).concat(checkForTableAccess(opts, connectionDbName, command, args, query_opts)); 146 | if (errorsFound.length > 0) return errorsFound; 147 | 148 | /*! 149 | * Check for unallowedTerms 150 | */ 151 | for (var key in terms) { 152 | if (terms.hasOwnProperty(key)) { 153 | var termName = terms[key]; 154 | if (_rethinkdbProtoDef2['default'].Term.TermType[termName] === command) return termName; 155 | } 156 | } 157 | if (command === _rethinkdbProtoDef2['default'].Term.TermType.MAKE_ARRAY) { 158 | return _lodash2['default'].flatten(query[1].map(__findTermsOrErrors)).filter(function (x) { 159 | return x; 160 | }); 161 | } 162 | return _lodash2['default'].flatten(query.map(__findTermsOrErrors)).filter(function (x) { 163 | return x; 164 | }); 165 | }; 166 | 167 | if (typeof query[2] === 'object' && query[2].db !== undefined) { 168 | connectionDbName = query[2].db[1][0]; 169 | termsFound = termsFound.concat(__findTermsOrErrors(query[2].db)); 170 | } 171 | return termsFound.concat(__findTermsOrErrors(query)); 172 | }; 173 | exports.findTermsOrErrors = findTermsOrErrors; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rethinkdb-proxy", 3 | "version": "0.0.8", 4 | "description": "Reverse Proxy for RethinkDB. Make your RethinkDB publicly accessible through limiting what kind of queries can be executed on your RethinkDB database.", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "bin": { 10 | "rethinkdb-proxy": "bin/rethinkdb-proxy.js" 11 | }, 12 | "scripts": { 13 | "test:es6": "export NODE_ENV='test_es6' && ./node_modules/mocha/bin/mocha --compilers js:babel/register ./test/index.js", 14 | "test": "export NODE_ENV='test' && npm run compile && ./node_modules/mocha/bin/mocha lib-test/index.js", 15 | "dev": "./node_modules/nodemon/bin/nodemon.js --exec babel-node -- ./src/index.js", 16 | "start": "node index.js", 17 | "compile": "npm run compile:src && npm run compile:test", 18 | "compile:src": "./node_modules/babel/bin/babel.js --optional runtime -d lib src", 19 | "compile:test": "./node_modules/babel/bin/babel.js --optional runtime -d lib-test test", 20 | "prepublish": "npm run compile" 21 | }, 22 | "author": "thejsj", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "bluebird": "^2.9.34", 26 | "mocha": "^2.2.5", 27 | "nodemon": "^1.3.7", 28 | "should": "^7.0.2" 29 | }, 30 | "dependencies": { 31 | "babel": "^5.6.23", 32 | "babel-runtime": "^5.8.20", 33 | "bunyan": "^1.5.1", 34 | "cli": "^0.8.0", 35 | "lodash": "^3.10.0", 36 | "rethinkdb": "^2.1.0", 37 | "taser": "^1.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/buffer-parser.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | const _listeners_ = Symbol('listeners'); 3 | const _queue_ = Symbol('queue'); 4 | 5 | const BufferParser = class BufferParser { 6 | 7 | constructor () { 8 | this[_queue_] = new Buffer(0); 9 | this[_listeners_] = {}; 10 | this.protoVersion = null; 11 | this.authKeyLength = null; 12 | this.authKey = null; 13 | this.protoProtocol = null; 14 | } 15 | 16 | append (buff) { 17 | if (buff !== undefined) { 18 | this[_queue_] = Buffer.concat([this[_queue_], buff]); 19 | } 20 | if (this.protoVersion === null) { 21 | if (Buffer.byteLength(this[_queue_]) >= 4) { 22 | this.protoVersion = this[_queue_].slice(0, 4).readUInt32LE(); 23 | this[_queue_] = this[_queue_].slice(4); 24 | // Call code again to ensure we don't have the authKey 25 | this.append(); 26 | } 27 | } else if (this.authKeyLength === null) { 28 | if (Buffer.byteLength(this[_queue_]) >= 4) { 29 | this.authKeyLength = this[_queue_].slice(0, 4).readUInt32LE(); 30 | this[_queue_] = this[_queue_].slice(4); 31 | // Call code again to ensure we don't have the authKey 32 | this.append(); 33 | } 34 | } else if (this.authKey === null) { 35 | if (Buffer.byteLength(this[_queue_] >= this.authKeyLength)) { 36 | this.authKey = this[_queue_].slice(0, this.authKeyLength).toString('ascii'); 37 | this[_queue_] = this[_queue_].slice(this.authKeyLength); 38 | // Call code again to ensure we don't have the authKey 39 | this.append(); 40 | } 41 | } else if (this.protoProtocol === null) { 42 | if (Buffer.byteLength(this[_queue_]) >= 4) { 43 | this.protoProtocol = this[_queue_].slice(0, 4).readUInt32LE(); 44 | this[_queue_] = this[_queue_].slice(4); 45 | // Call code again to ensure we don't have the authKey 46 | this.append(); 47 | this.fireEvent('connect', this.protoVersion, this.authKey, this.protoProtocol); 48 | } 49 | } else { 50 | this.parseQuery(); 51 | } 52 | } 53 | 54 | fireEvent (eventName, ...data) { 55 | if (this[_listeners_][eventName] !== undefined) { 56 | this[_listeners_][eventName].forEach((func) => { 57 | func.apply(null, data); 58 | }); 59 | } 60 | } 61 | 62 | parseQuery () { 63 | /*! 64 | * Assumptions 65 | * 1. All clients have their own instance of BufferParser 66 | * 2. TCP connections send messages in order 67 | * 3. Because messages are sent in order by the client through a TCP connection, 68 | * we can assume that all packets are tokens followed by query bytelenghts 69 | * followed by queries 70 | */ 71 | let newQueue = null; 72 | let _prevQuery = null; 73 | /*! 74 | * Make sure we have all three components necessary to parse the query: 75 | * token, byteLength, and query 76 | */ 77 | if (Buffer.byteLength(this[_queue_]) <= 12) return; 78 | let token = this[_queue_].slice(0, 8).readUInt32LE(); 79 | let byteLength = this[_queue_].slice(8, 12).readUInt32LE(); 80 | let query = this[_queue_].slice(12, byteLength + 12); 81 | newQueue = this[_queue_].slice(byteLength + 12); 82 | try { 83 | // Simplest way to handle check if input is valid JSON 84 | let json = JSON.parse(query.toString()); 85 | this.fireEvent('query', json, token); 86 | } catch (err) { 87 | // I think the problem has something to do with the fact that I'm adding a 88 | // comma somehwere .... 89 | this.fireEvent('error', token); 90 | } 91 | this[_queue_] = new Buffer(0); 92 | if (newQueue !== null && newQueue.length > 0 && Buffer.byteLength(newQueue) >= 8){ 93 | let token = newQueue.slice(0, 8).readUInt32LE(); 94 | if (parseInt(token) === +token) { 95 | this.append(newQueue); 96 | } 97 | } 98 | } 99 | 100 | on (eventName, func) { 101 | if (this[_listeners_][eventName] === undefined) { 102 | this[_listeners_][eventName] = []; 103 | } 104 | this[_listeners_][eventName].push(func); 105 | } 106 | 107 | }; 108 | 109 | export default BufferParser; 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true, bitwise:false */ 2 | import 'babel/polyfill'; 3 | import net from 'net'; 4 | import protoDef from 'rethinkdb/proto-def'; 5 | import BufferParser from './buffer-parser'; 6 | import { findTermsOrErrors } from './query-parser'; 7 | import optionsParser from './options-parser'; 8 | import Logger from './logger.js'; 9 | 10 | export default class RethinkDBProxy { 11 | 12 | constructor (opts) { 13 | // Set defaults and unallowedTerms 14 | this.opts = optionsParser(opts); 15 | this.logger = new Logger(opts.inLevel, opts.outLevel, opts.sysLevel); 16 | this.__connections = []; 17 | this.server = net.createServer(this.connectionHandler.bind(this)); 18 | return this; 19 | } 20 | 21 | listen (cb) { 22 | return new Promise((resolve, reject) => { 23 | this.server.listen(this.opts.port, (err) => { 24 | if (err) { 25 | this.logger.sys.info({ err: err, port: this.opts.port }, 'Error tryingg to listen for traffic'); 26 | if (typeof cb === 'function') cb(err); 27 | reject(err); 28 | } 29 | this.logger.sys.info({ port: this.opts.port }, 'server.listen'); 30 | if (typeof cb === 'function') cb(); 31 | resolve(); 32 | }); 33 | }); 34 | } 35 | 36 | close (cb) { 37 | return new Promise((resolve, reject) => { 38 | this.server.close(() => { 39 | this.logger.sys.info('server.close'); 40 | if (typeof cb === 'function') cb(); 41 | resolve(); 42 | }); 43 | setTimeout(() => { 44 | this.logger.sys.info('Server closing all connections'); 45 | this.__connections.forEach((conn) => { 46 | this.logger.sys.trace({ conn: conn }, 'conn.destroy'); 47 | conn.destroy(); 48 | }); 49 | }, 100); 50 | }); 51 | } 52 | 53 | makeSendResponse (socket) { 54 | return (response, token) => { 55 | this.logger.out.info({ 56 | token: token, 57 | response: response 58 | }, 'Send Response'); 59 | let tokenBuffer = new Buffer(8); 60 | tokenBuffer.writeUInt32LE(token & 0xFFFFFFFF, 0); 61 | tokenBuffer.writeUInt32LE(Math.floor(token / 0xFFFFFFFF), 4); 62 | let responseBuffer = new Buffer(JSON.stringify(response)); 63 | let lengthBuffer = new Buffer(4); 64 | lengthBuffer.writeUInt32LE(responseBuffer.length, 0); 65 | if (socket.destroyed) return; 66 | socket.write(tokenBuffer); 67 | socket.write(lengthBuffer); 68 | socket.write(responseBuffer); 69 | }; 70 | } 71 | 72 | makeSendError (clientSocket) { 73 | let sendResponseToClient = this.makeSendResponse(clientSocket); 74 | return (token, message) => { 75 | let response = { 76 | t: protoDef.Response.ResponseType.CLIENT_ERROR, 77 | b: [], 78 | n: [], 79 | r: [message] 80 | }; 81 | this.logger.out.info({ 82 | message: message, 83 | response: response, 84 | token: token 85 | }, 'Send Error'); 86 | sendResponseToClient(response, token); 87 | }; 88 | } 89 | 90 | connectionHandler (clientSocket) { 91 | 92 | this.__connections.push(clientSocket); 93 | 94 | clientSocket.connected = false; 95 | let parser = new BufferParser(); 96 | let serverSocket = net.connect(this.opts.rdbPort, this.opts.rdbHost); 97 | let sendResponseToServer = this.makeSendResponse(serverSocket); 98 | let sendResponseToClient = this.makeSendResponse(clientSocket); 99 | let sendError = this.makeSendError(clientSocket); 100 | 101 | /*! 102 | * Listeners 103 | */ 104 | serverSocket.on('data', (buff) => { 105 | this.logger.out.info({ 106 | buffer: buff.toString(), 107 | }, 'Data received'); 108 | if (buff.toString() === 'SUCCESS') clientSocket.connected = true; 109 | // NOTE: The socket might try to write something even if the connection 110 | // is closed 111 | if (clientSocket.destroyed) return; 112 | clientSocket.write(buff); 113 | }); 114 | 115 | parser.on('connect', (version, authKey, protoProtocol) => { 116 | this.logger.in.info({ 117 | version: version, 118 | authKey: authKey, 119 | protoProtocol: protoProtocol 120 | }, 'parser.on.connect'); 121 | let versionBuffer = new Buffer(4); 122 | versionBuffer.writeUInt32LE(version, 0); 123 | let authBuffer = new Buffer(authKey, 'ascii'); 124 | let authLengthBuffer = new Buffer(4); 125 | authLengthBuffer.writeUInt32LE(authBuffer.length, 0); 126 | let protocolBuffer = new Buffer(4); 127 | protocolBuffer.writeUInt32LE(protoProtocol, 0); 128 | let token = Buffer.concat([versionBuffer, authLengthBuffer, authBuffer, protocolBuffer]); 129 | if (protoProtocol !== protoDef.VersionDummy.Protocol.JSON) { 130 | sendError(token, 'Proxy Error: Only JSON protocol allowed.'); 131 | } 132 | serverSocket.write(token); 133 | }); 134 | 135 | parser.on('error', (token) => { 136 | this.logger.in.error({ 137 | token: token, 138 | }, 'parser.on.error'); 139 | sendError('Proxy Error: Could not parse query correctly.'); 140 | }); 141 | 142 | parser.on('query', (query, token) => { 143 | this.logger.in.info({ 144 | query: query, 145 | token: token, 146 | }, 'parser.on.query'); 147 | let termsFound = findTermsOrErrors(this.opts, this.opts.unallowedTerms, query); 148 | if (termsFound.length > 0) { 149 | // This shouldn't throw an error. It should 150 | // send the error through the TCP connection 151 | let errorMessage; 152 | if (typeof termsFound[0] === 'object' && typeof termsFound[0].error === 'string') { 153 | errorMessage = termsFound[0].error; 154 | } else { 155 | errorMessage = 'Cannot execute query. \"' + termsFound + '\" query not allowed.'; 156 | } 157 | this.logger.in.warn({ 158 | termsFound: termsFound, 159 | errorMessage: errorMessage, 160 | }, 'parser.on.query Error'); 161 | sendError(token, errorMessage); 162 | } 163 | return sendResponseToServer(query, token); 164 | }); 165 | 166 | clientSocket.on('data', (data) => { 167 | this.logger.in.trace({ 168 | data: data, 169 | }, 'clientSocket.on.data'); 170 | return parser.append(data); 171 | }); 172 | } 173 | } 174 | 175 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan'; 2 | 3 | const _log_ = Symbol('log'); 4 | 5 | export const levels = { 6 | 10: 'trace', 7 | 20: 'debug', 8 | 30: 'info', 9 | 40: 'warn', 10 | 50: 'error', 11 | 60: 'fatal' 12 | }; 13 | 14 | class LevelLogger { 15 | 16 | constructor (globalLevel, logger) { 17 | this[_log_] = logger; 18 | // "fatal" (60): The service/app is going to stop or become unusable now. 19 | // An operator should definitely look into this soon. 20 | // "error" (50): Fatal for a particular request, but the service/app continues servicing other requests. 21 | // An operator should look at this soon(ish). 22 | // "warn" (40): A note on something that should probably be looked at by an operator eventually. 23 | // "info" (30): Detail on regular operation. 24 | // "debug" (20): Anything else, i.e. too verbose to be included in "info" level. 25 | // "trace" (10): Logging from external libraries used by your app or very detailed application logging. 26 | Object.keys(levels).forEach(levelKey => { 27 | this[levelKey] = this.log(globalLevel, levelKey, levels[levelKey]); 28 | this[levels[levelKey]] = this.log(globalLevel, levelKey, levels[levelKey]); 29 | }); 30 | } 31 | 32 | log (globalLevel, level, levelName) { 33 | return (...args) => { 34 | this[_log_][levelName].apply(this[_log_], args); 35 | }; 36 | } 37 | } 38 | 39 | export default class Logger { 40 | 41 | constructor (inLevel, outLevel, sysLevel) { 42 | let logger = bunyan.createLogger({ name: 'rethinkdb-proxy'}); 43 | this.in = new LevelLogger(inLevel, logger); 44 | this.out = new LevelLogger(outLevel, logger); 45 | this.sys = new LevelLogger(sysLevel, logger); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/options-parser.js: -------------------------------------------------------------------------------- 1 | import taser from 'taser'; 2 | import { levels } from './logger.js'; 3 | 4 | const assertStringOrArray = taser(['string', 'array']); 5 | const assertBoolean = taser(['boolean']); 6 | const assertNumber = taser(['number']); 7 | const assertString = taser(['string']); 8 | const assertLevelString = taser({ 9 | type: 'string', 10 | values: (() => { 11 | return Object.keys(levels).map(levelKey => levels[levelKey]); 12 | })() 13 | }); 14 | 15 | let getAllowedTermsOpts = (opts) => { 16 | // By default, don't allow any of these terms 17 | opts.unallowedTerms = [ 18 | 'INSERT', 19 | 'UPDATE', 20 | 'REPLACE', 21 | 'DELETE', 22 | 'DB_CREATE', 23 | 'DB_DROP', 24 | 'TABLE_CREATE', 25 | 'TABLE_DROP', 26 | 'INDEX_CREATE', 27 | 'INDEX_DROP', 28 | 'INDEX_RENAME', 29 | 'RECONFIGURE', 30 | 'REBALANCE', 31 | 'HTTP', 32 | 'JAVASCRIPT' 33 | ]; 34 | 35 | let toUpperCaseSnakeCase = (str) => { 36 | return str 37 | .replace(/(^[A-Z])/g, ($1) => { return $1.toLowerCase(); }) 38 | .replace(/([A-Z])/g, ($1) => { return '_'+$1.toUpperCase(); }) 39 | .toUpperCase(); 40 | }; 41 | let allowTerm = (termName) => { 42 | if (opts.unallowedTerms.includes(termName)) { 43 | opts.unallowedTerms.splice(opts.unallowedTerms.indexOf(termName), 1); 44 | } 45 | }; 46 | // Allow all terms specified by user (single terms) 47 | for(let key in opts) { 48 | if (opts.hasOwnProperty(key)) { 49 | if (opts[key] === true && key.substring(0, 5) === 'allow') { 50 | let termName = toUpperCaseSnakeCase(key.substring(5)); 51 | allowTerm(termName); 52 | } 53 | } 54 | } 55 | // Allow all terms specified (multiple terms) 56 | if (opts.allowWrites) { 57 | allowTerm('INSERT'); 58 | allowTerm('UPDATE'); 59 | allowTerm('DELETE'); 60 | } 61 | if (opts.allowIndexes) { 62 | allowTerm('INDEX_CREATE'); 63 | allowTerm('INDEX_DROP'); 64 | allowTerm('INDEX_RENAME'); 65 | } 66 | return opts; 67 | }; 68 | 69 | let getAllowedDatabaseOpts = (opts) => { 70 | // Clean options 71 | if (typeof opts.dbs === 'string') opts.dbs = [opts.dbs]; 72 | if (typeof opts.tables === 'string') opts.tables = [opts.tables]; 73 | 74 | // Create object for dbs 75 | let databases = opts.dbs; 76 | opts.dbs = { 77 | $$count: 0 // `$` not allowed in RethinkDB database names 78 | }; 79 | databases.forEach(function (database) { 80 | opts.dbs[database] = { allowed: true, tables: { }, $$count: 0 }; 81 | // `$` not allowed in RethinkDB database names 82 | opts.dbs.$$count += 1; 83 | }); 84 | opts.tables.forEach(function (tableName) { 85 | if (opts.dbs.$$count > 1) { 86 | let split = tableName.split('.'); 87 | if (split.length !== 2) { 88 | let message = `If more than 1 database is passed, `; 89 | message += `all table names must be dot (\`.\`) separated with the table names.`; 90 | message += ` \`${tableName}\` is not valid.`; 91 | throw new Error(message); 92 | } 93 | if (opts.dbs[split[0]] === undefined) { 94 | let message = `Database ${split[0]} in ${tableName} was not declared`; 95 | throw new Error(message); 96 | } 97 | opts.dbs[split[0]].tables[split[1]] = { allowed: true }; 98 | opts.dbs[split[0]].$$count += 1; 99 | } 100 | }); 101 | return opts; 102 | }; 103 | 104 | let parseLoggingOpts = (opts) => { 105 | // Set default levels 106 | if (opts.logLevel === undefined) opts.logLevel = 'fatal'; 107 | if (opts.logLevelIn === undefined) opts.logLevelIn = opts.logLevel; 108 | if (opts.logLevelOut === undefined) opts.logLevelOut = opts.logLevel; 109 | if (opts.logLevelSys === undefined) opts.logLevelSys = opts.logLevel; 110 | let levelsStringToInts = Object.keys(levels).reduce((keysObj, key) => { 111 | let obj = {}; 112 | obj[levels[key]] = key; 113 | return Object.assign(keysObj, obj); 114 | }, {}); 115 | opts.logLevel = levelsStringToInts[opts.logLevel]; 116 | opts.logLevelIn = levelsStringToInts[opts.logLevelIn]; 117 | opts.logLevelOut = levelsStringToInts[opts.logLevelOut]; 118 | opts.logLevelSys = levelsStringToInts[opts.logLevelSys]; 119 | return opts; 120 | }; 121 | 122 | export default (opts) => { 123 | // Define Options and defaults 124 | opts = Object.assign({ 125 | port: 8125, 126 | rdbHost: 'localhost', 127 | rdbPort: 28015, 128 | logLevel: 'fatal', 129 | logLevelIn: 'fatal', 130 | logLevelOut: 'fatal', 131 | logLevelSys: 'fatal', 132 | dbs: [], 133 | tables: [], 134 | allowSysDbAccess: false, 135 | allowWrites: false, // Allow insert, update, delete 136 | allowInsert: false, 137 | allowUpdate: false, 138 | allowReplace: false, 139 | allowDelete: false, 140 | allowDbCreate: false, 141 | allowDbDrop: false, 142 | allowTableCreate: false, 143 | allowTableDrop: false, 144 | allowIndexes: false, // Allow indexCreate, indexDrop, indexRename 145 | allowIndexCreate: false, 146 | allowIndexDrop: false, 147 | allowIndexRename: false, 148 | allowReconfigure: false, 149 | allowRebalance: false, 150 | allowHttp: false, 151 | allowJavascript: false 152 | }, opts); 153 | 154 | // Ensure validity of inputs 155 | assertNumber(opts.port); 156 | assertNumber(opts.rdbPort); 157 | assertString(opts.rdbHost); 158 | assertLevelString(opts.logLevel); 159 | assertLevelString(opts.logLevelIn); 160 | assertLevelString(opts.logLevelOut); 161 | assertLevelString(opts.logLevelSys); 162 | assertStringOrArray(opts.dbs); 163 | assertStringOrArray(opts.tables); 164 | 165 | // Assert booleans 166 | for (let key in opts) { 167 | if (opts.hasOwnProperty(key) && key.substring(0, 5) === 'allow') { 168 | assertBoolean(opts[key]); 169 | } 170 | } 171 | 172 | // Parse logging options 173 | opts = parseLoggingOpts(opts); 174 | 175 | // Get allowed databases 176 | opts = getAllowedDatabaseOpts(opts); 177 | 178 | // Get allowed terms 179 | opts = getAllowedTermsOpts(opts); 180 | 181 | return opts; 182 | }; 183 | -------------------------------------------------------------------------------- /src/query-parser.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import protoDef from 'rethinkdb/proto-def'; 3 | 4 | let isRQLQuery = (query) => { 5 | // Duck typing a query... 6 | if (!Array.isArray(query)) return false; 7 | if (query.length < 2 || query.length > 3) return false; 8 | if (!Number.isInteger(query[0])) return false; 9 | if (query[2] !== undefined && typeof query[2] !== 'object' && !Array.isArray(query[2])) { 10 | return false; 11 | } 12 | return true; 13 | }; 14 | 15 | let checkForDeleteInReplace = function (terms, command, args, query_opts) { 16 | if ( 17 | command === protoDef.Term.TermType.REPLACE && 18 | !terms.includes('REPLACE') && 19 | terms.includes('DELETE') 20 | ){ 21 | let argPassedToReplace = args[args.length - 1]; 22 | if (argPassedToReplace === null) { 23 | return [{ 24 | 'error': 'Using the `REPLACE` term with `null` is not allowed if `DELETE` is not also allowed.' 25 | }]; 26 | } 27 | } 28 | return []; 29 | }; 30 | 31 | let checkForUpdateInInsert = function (terms, command, args, query_opts) { 32 | if ( 33 | command === protoDef.Term.TermType.INSERT && 34 | !terms.includes('INSERT') && 35 | terms.includes('UPDATE') 36 | ){ 37 | if ( 38 | typeof query_opts === 'object' && 39 | typeof query_opts.conflict === 'string' && 40 | query_opts.conflict.toLowerCase() === 'update' 41 | ) { 42 | return [{ 43 | 'error': 'Using the `INSERT` term with `conflict: update` is not allowed if `UPDATE` is not also allowed.' 44 | }]; 45 | } 46 | } 47 | return []; 48 | }; 49 | 50 | let checkForDeleteInInsert = function (terms, command, args, query_opts) { 51 | if ( 52 | command === protoDef.Term.TermType.INSERT && 53 | !terms.includes('INSERT') && 54 | terms.includes('REPLACE') 55 | ){ 56 | if ( 57 | typeof query_opts === 'object' && 58 | typeof query_opts.conflict === 'string' && 59 | query_opts.conflict.toLowerCase() === 'replace' 60 | ) { 61 | return [{ 62 | 'error': 'Using the `INSERT` term with `conflict: replace` is not allowed if `REPLACE` is not also allowed.' 63 | }]; 64 | } 65 | } 66 | return []; 67 | }; 68 | 69 | let checkForTableAccess = function (opts, connectionDbName, command, args, query_opts) { 70 | if (command === protoDef.Term.TermType.TABLE && typeof opts === 'object') { 71 | let tableName = args[args.length - 1]; 72 | let dbName = connectionDbName; 73 | /*! 74 | * Since the name of a table can be passed dynamically and this introducues 75 | * a lot of complexity to determining whether a tables is allowed, only 76 | * allow strings, which cover most cases 77 | */ 78 | if (typeof tableName !== 'string') { 79 | return [{ 80 | 'error': `Only strings are allowed for table names while using the \`table\` command` 81 | }]; 82 | } 83 | if (opts.dbs.$$count > 0 && (typeof opts.dbs[dbName] === 'object' && opts.dbs[dbName].allowed)) { 84 | if ( 85 | opts.dbs[dbName].$$count > 0 && 86 | (typeof opts.dbs[dbName].tables[tableName] !== 'object' || 87 | !opts.dbs[dbName].tables[tableName].allowed) 88 | ) { 89 | return [{ 'error': `Access to the \`{$tableName}\` table is not allowed.` + 90 | ` Table must be declared in the \`tables\` and` + 91 | ` database must be inlcluded in \`dbs\` parameter` 92 | }]; 93 | } 94 | } 95 | } 96 | return []; 97 | }; 98 | 99 | let checkForDatabaseAccess = function (opts, connectionDbName, command, args, query_opts) { 100 | if (command === protoDef.Term.TermType.DB && typeof opts === 'object') { 101 | let dbName = args[args.length - 1]; 102 | /*! 103 | * Since the name of a database can be passed dynamically and this introduces 104 | * a lot of complexity to determining whether a tables is allowed, only 105 | * allow strings, which cover most cases 106 | */ 107 | if (typeof dbName !== 'string') { 108 | return [{ 109 | 'error': `Only strings are allowed for database names while using the \`db\` command` 110 | }]; 111 | } 112 | if (!opts.allowSysDbAccess && dbName === 'rethinkdb') { 113 | return [{ 114 | 'error': 'Access to the `rethinkdb` database is not allowed unless explicitly stated with `allowSysDbAccess`' 115 | }]; 116 | } 117 | if (opts.dbs.$$count > 0 && (typeof opts.dbs[dbName] !== 'object' || !opts.dbs[dbName].allowed)) { 118 | return [{ 'error': `Access to the \`${dbName}\` database is not allowed. ` + 119 | `Database must be inlcluded in \`db\` parameter` }]; 120 | } 121 | } 122 | return []; 123 | }; 124 | 125 | export const findTermsOrErrors = (opts, terms, query) => { 126 | let termsFound = [], connectionDbName; 127 | 128 | const __findTermsOrErrors = (query) => { 129 | if (!isRQLQuery(query)) { 130 | if (Array.isArray(query)) { 131 | return _.flatten(query.map(__findTermsOrErrors)).filter(x => x); 132 | } 133 | return []; 134 | } 135 | let command = query[0], args = query[1], query_opts = query[2]; 136 | 137 | /*! 138 | * Edge cases 139 | */ 140 | let errorsFound = [] 141 | // #1 If `replace` is allowed but `delete` is not 142 | .concat(checkForDeleteInReplace(terms, command, args, query_opts)) 143 | // #2 If `insert` is allowed but `update` is not 144 | .concat(checkForUpdateInInsert(terms, command, args, query_opts)) 145 | // #3 If `insert` is allowed but `delete` is not 146 | .concat(checkForDeleteInInsert(terms, command, args, query_opts)); 147 | if (errorsFound.length > 0) return errorsFound; 148 | 149 | /*! 150 | * Database and table access 151 | */ 152 | errorsFound = [] 153 | .concat(checkForDatabaseAccess(opts, connectionDbName, command, args, query_opts)) 154 | .concat(checkForTableAccess(opts, connectionDbName, command, args, query_opts)); 155 | if (errorsFound.length > 0) return errorsFound; 156 | 157 | /*! 158 | * Check for unallowedTerms 159 | */ 160 | for (let key in terms) { 161 | if (terms.hasOwnProperty(key)) { 162 | let termName = terms[key]; 163 | if(protoDef.Term.TermType[termName] === command) return termName; 164 | } 165 | } 166 | if (command === protoDef.Term.TermType.MAKE_ARRAY) { 167 | return _.flatten(query[1].map(__findTermsOrErrors)).filter(x => x); 168 | } 169 | return _.flatten(query.map(__findTermsOrErrors)).filter(x => x); 170 | }; 171 | 172 | if (typeof query[2] === 'object' && query[2].db !== undefined) { 173 | connectionDbName = query[2].db[1][0]; 174 | termsFound = termsFound.concat(__findTermsOrErrors(query[2].db)); 175 | } 176 | return termsFound.concat(__findTermsOrErrors(query)); 177 | }; 178 | 179 | 180 | -------------------------------------------------------------------------------- /test/changefeeds.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | import { makeExecuteQuery, makeExecuteProxyQuery, makeAssertQuery, makeCreateDatabase, makeDropDatabase } from './utils'; 7 | import protoDef from '../node_modules/rethinkdb/proto-def'; 8 | 9 | let proxyPort = 8125; 10 | let dbName = 'rethinkdb_proxy_test'; 11 | let secondDbName = 'rethinkdb_proxy_test_2'; 12 | let tableName = 'entries'; 13 | let server; 14 | 15 | let executeQuery = makeExecuteQuery(dbName, proxyPort); 16 | let executeProxyQuery = makeExecuteProxyQuery(dbName, proxyPort); 17 | let assertQuery = makeAssertQuery(executeQuery); 18 | let createDatabase = makeCreateDatabase(dbName, tableName); 19 | let createSecondDatabase = makeCreateDatabase(secondDbName, tableName); 20 | let dropDatabase = makeDropDatabase(dbName); 21 | let throwError = function (res) { throw new Error(); }; 22 | let expectError = function (errorName, errorMessageMatch, err) { 23 | if (errorName !== null) errorName.should.equal(err.name); 24 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 25 | (err instanceof Error).should.equal(true); 26 | }; 27 | let closeCursorAndConn = function (cursor, conn) { 28 | return cursor.close() 29 | .then(function () { 30 | return conn.close(); 31 | }); 32 | }; 33 | let ce = function (err) { 34 | console.log('Error'); 35 | console.log(err); 36 | throw err; 37 | }; 38 | 39 | describe('Changefeeds', () => { 40 | 41 | before(function (done) { 42 | this.timeout(5000); 43 | createDatabase() 44 | .then(() => { 45 | return new Promise(function (resolve, reject) { 46 | server = new RethinkDBProxy({ 47 | port: proxyPort, 48 | allowInsert: true 49 | }); 50 | server.listen(resolve); 51 | }); 52 | }) 53 | .nodeify(done); 54 | }); 55 | 56 | after((done) => { 57 | dropDatabase() 58 | .then(server.close.bind(server)) 59 | .then(done.bind(null, null)); 60 | }); 61 | 62 | it('should listen for changes', function (done) { 63 | this.timeout(10000); 64 | var count = 0, results = [], cursor; 65 | // HINT: You'll need to pass an anonymous function to `filter` 66 | r.connect({ port: proxyPort, db: dbName }).then((conn) => { 67 | return r.table(tableName).changes().run(conn) 68 | .then(function (_cursor) { 69 | cursor = _cursor; 70 | cursor.each(function (err, row) { 71 | count += 1; 72 | results.push(row); 73 | }); 74 | }) 75 | .then(function () { 76 | return r.connect({ port: proxyPort, db: dbName }).then((conn) => { 77 | return Promise.all([ 78 | r.table(tableName).insert({ name: 'SomeCountryName', population: 1 }).run(conn).catch(ce), 79 | r.table(tableName).insert({ name: 'Transylvania', population: 2000 }).run(conn).catch(ce), 80 | r.table(tableName).insert({ name: 'Hong Kong', population: 1500 }).run(conn), 81 | r.table(tableName).insert({ name: 'Bavaira', population: 98 }).run(conn), 82 | ]) 83 | .then(conn.close.bind(conn)); 84 | }); 85 | }) 86 | .then(function () { 87 | return new Promise(function (resolve, reject) { 88 | setTimeout(function () { 89 | count.should.equal(4); 90 | results.should.be.instanceOf(Array); 91 | results = results.sort((a, b) => a.new_val.population - b.new_val.population); 92 | results[0].new_val.population.should.equal(1); 93 | results[1].new_val.population.should.equal(98); 94 | results[0].new_val.should.have.property('name'); 95 | results[1].new_val.should.have.property('name'); 96 | resolve(); 97 | }, 50); // Hopefully, this is enough 98 | }); 99 | }) 100 | .then(function () { 101 | return closeCursorAndConn(cursor, conn); 102 | }) 103 | .nodeify(done); 104 | }); 105 | }); 106 | 107 | it('should listen for multiple changesfeeds', function (done) { 108 | this.timeout(10000); 109 | var first_count = 0, second_count = 0, results = [], cursor; 110 | return Promise.all([ 111 | r.connect({ port: proxyPort, db: dbName }).then((conn) => { 112 | return r.table(tableName).filter(r.row('population').lt(500)).changes().run(conn) 113 | .then(function (cursor) { 114 | cursor.each(function (err, row) { 115 | first_count += 1; 116 | results.push(row); 117 | }, conn.close.bind(conn)); 118 | }); 119 | }), 120 | r.connect({ port: proxyPort, db: dbName }).then((conn) => { 121 | return r.table(tableName).filter(r.row('population').gt(500)).changes().run(conn) 122 | .then(function (cursor) { 123 | cursor.each(function (err, row) { 124 | second_count += 1; 125 | results.push(row); 126 | }, conn.close.bind(conn)); 127 | }); 128 | }) 129 | ]) 130 | .then(function () { 131 | return r.connect({ port: proxyPort, db: dbName }).then((conn) => { 132 | return Promise.all([ 133 | r.table(tableName).insert({ name: 'SomeCountryName', population: 1 }).run(conn).catch(ce), 134 | r.table(tableName).insert({ name: 'Transylvania', population: 2000 }).run(conn).catch(ce), 135 | r.table(tableName).insert({ name: 'Hong Kong', population: 1500 }).run(conn), 136 | r.table(tableName).insert({ name: 'Bavaira', population: 98 }).run(conn), 137 | ]) 138 | .then(conn.close.bind(conn)); 139 | }); 140 | }) 141 | .then(function () { 142 | return new Promise(function (resolve, reject) { 143 | setTimeout(function () { 144 | first_count.should.equal(2); 145 | second_count.should.equal(2); 146 | results.should.be.instanceOf(Array); 147 | results = results.sort((a, b) => a.new_val.population - b.new_val.population); 148 | results[0].new_val.population.should.equal(1); 149 | results[1].new_val.population.should.equal(98); 150 | results[0].new_val.should.have.property('name'); 151 | results[1].new_val.should.have.property('name'); 152 | resolve(); 153 | }, 50); // Hopefully, this is enough 154 | }); 155 | }) 156 | .nodeify(done); 157 | }); 158 | 159 | }); 160 | -------------------------------------------------------------------------------- /test/components/buffer-parser.js: -------------------------------------------------------------------------------- 1 | var BufferParser; 2 | if (process.env.NODE_ENV === 'test_es6') { 3 | // Called from /test 4 | var BufferParserES6 = require('../../src/buffer-parser'); 5 | BufferParser = BufferParserES6; 6 | } else { 7 | // Called from /lib/test 8 | var BufferParserES5 = require('../../lib/buffer-parser'); 9 | BufferParser = BufferParserES5; 10 | } 11 | export default BufferParser; 12 | -------------------------------------------------------------------------------- /test/components/rethinkdb-proxy.js: -------------------------------------------------------------------------------- 1 | var RethinkDBProxy; 2 | if (process.env.NODE_ENV === 'test_es6') { 3 | // Called from /test 4 | var RethinkDBProxyES6 = require('../../src'); 5 | RethinkDBProxy = RethinkDBProxyES6; 6 | } else { 7 | // Called from /lib/test 8 | var RethinkDBProxyES5 = require('../../lib'); 9 | RethinkDBProxy = RethinkDBProxyES5; 10 | } 11 | export default RethinkDBProxy; 12 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | 7 | let proxyPort = 8125; 8 | let server; 9 | 10 | describe('Connection', () => { 11 | 12 | before((done) => { 13 | server = new RethinkDBProxy({ 14 | port: proxyPort 15 | }); 16 | server.listen(done); 17 | }); 18 | 19 | after((done) => { 20 | server.close().then(done.bind(null, null)); 21 | }); 22 | 23 | it('should create a connection successfully', (done) => { 24 | Promise.resolve() 25 | .then(() => { 26 | return [r.connect(), r.connect({ port: proxyPort })]; 27 | }) 28 | .spread((connA, connB) => { 29 | connA.port.should.equal(28015); 30 | connB.port.should.equal(proxyPort); 31 | connA.constructor.should.equal(connB.constructor); 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/database-and-table-access.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | import { makeExecuteQuery, makeExecuteProxyQuery, makeAssertQuery, makeCreateDatabase, makeDropDatabase } from './utils'; 7 | import protoDef from '../node_modules/rethinkdb/proto-def'; 8 | 9 | let proxyPort = 8125; 10 | let dbName = 'rethinkdb_proxy_test'; 11 | let secondDbName = 'rethinkdb_proxy_test_2'; 12 | let tableName = 'entries'; 13 | let server; 14 | 15 | let executeQuery = makeExecuteQuery(dbName, proxyPort); 16 | let executeProxyQuery = makeExecuteProxyQuery(dbName, proxyPort); 17 | let assertQuery = makeAssertQuery(executeQuery); 18 | let createDatabase = makeCreateDatabase(dbName, tableName); 19 | let createSecondDatabase = makeCreateDatabase(secondDbName, tableName); 20 | let dropDatabase = makeDropDatabase(dbName); 21 | let throwError = function (res) { throw new Error(); }; 22 | let expectError = function (errorName, errorMessageMatch, err) { 23 | if (errorName !== null) errorName.should.equal(err.name); 24 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 25 | (err instanceof Error).should.equal(true); 26 | }; 27 | 28 | describe('Database and Table Access', () => { 29 | 30 | let get = r.db(dbName).table(tableName); 31 | 32 | describe('allowSysDbAccess', () => { 33 | 34 | describe('Disallow RethinkDB Database Access', () => { 35 | 36 | before(function (done) { 37 | this.timeout(10000); 38 | createDatabase() 39 | .then(() => { 40 | return new Promise(function (resolve, reject) { 41 | server = new RethinkDBProxy({ 42 | port: proxyPort, 43 | }); 44 | server.listen(resolve); 45 | }); 46 | }) 47 | .nodeify(done); 48 | }); 49 | 50 | after((done) => { 51 | dropDatabase() 52 | .then(server.close.bind(server)) 53 | .then(done.bind(null, null)); 54 | }); 55 | 56 | it('should not allow a query that uses the `rethinkdb` database', (done) => { 57 | executeQuery(r.db('rethinkdb').tableList()) 58 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 59 | .nodeify(done); 60 | }); 61 | 62 | it('should not allow a query that uses the `rethinkdb` database inside a `do`', (done) => { 63 | executeQuery(r.expr(true).do(function () { 64 | return r.db('rethinkdb').tableList(); 65 | })) 66 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 67 | .nodeify(done); 68 | }); 69 | 70 | it('should not allow a query that uses the `rethinkdb` database inside a `do` inside a `do`', (done) => { 71 | executeQuery(r.expr([1, 2, 3]).do(function (arr) { 72 | return r.expr(arr).do(function () { 73 | return r.db('rethinkdb').tableList(); 74 | }); 75 | })) 76 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 77 | .nodeify(done); 78 | }); 79 | 80 | it('should not allow a query that passes null to `replace` if `delete` is not allowed', (done) => { 81 | executeQuery(r.db('rethinkdb').table('server_config')) 82 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 83 | .nodeify(done); 84 | }); 85 | 86 | }); 87 | 88 | describe('Allow RethinkDB Database Access', () => { 89 | 90 | before(function (done) { 91 | this.timeout(10000); 92 | createDatabase() 93 | .then(() => { 94 | return new Promise(function (resolve, reject) { 95 | server = new RethinkDBProxy({ 96 | port: proxyPort, 97 | allowSysDbAccess: true 98 | }); 99 | server.listen(resolve); 100 | }); 101 | }) 102 | .nodeify(done); 103 | }); 104 | 105 | after((done) => { 106 | dropDatabase() 107 | .then(server.close.bind(server)) 108 | .then(done.bind(null, null)); 109 | }); 110 | 111 | it('should allow a query that uses the `rethinkdb` database, if explicitly set', (done) => { 112 | executeQuery(r.db('rethinkdb').tableList()) 113 | .then(function (res) { 114 | Array.isArray(res).should.equal(true); 115 | res.length.should.not.be.equal(0); 116 | }) 117 | .nodeify(done); 118 | }); 119 | 120 | it('should allow a query queries the `rehtinkdb` database if explicitly set', (done) => { 121 | executeQuery(r.db('rethinkdb').table('server_config').coerceTo('array')) 122 | .then(function (res) { 123 | Array.isArray(res).should.equal(true); 124 | res.length.should.not.be.equal(0); 125 | }) 126 | .nodeify(done); 127 | }); 128 | }); 129 | 130 | }); 131 | 132 | describe('Database Access', () => { 133 | before(function (done) { 134 | this.timeout(10000); 135 | createDatabase() 136 | .then(createSecondDatabase) 137 | .then(() => { 138 | return new Promise(function (resolve, reject) { 139 | server = new RethinkDBProxy({ 140 | port: proxyPort, 141 | dbs: [dbName, 'someOtherDb'], 142 | }); 143 | server.listen(resolve); 144 | }); 145 | }) 146 | .nodeify(done); 147 | }); 148 | 149 | after((done) => { 150 | dropDatabase() 151 | .then(server.close.bind(server)) 152 | .then(done.bind(null, null)); 153 | }); 154 | 155 | 156 | it('should not allow access to a database that was not allowed', (done) => { 157 | executeProxyQuery(r.db(secondDbName).tableList()) 158 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 159 | .nodeify(done); 160 | }); 161 | 162 | it('should not allow access to a database that was not allowed inside a `do`', (done) => { 163 | executeProxyQuery(r.expr(1).do(function () { 164 | return r.db(secondDbName).tableList(); 165 | })) 166 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 167 | .nodeify(done); 168 | }); 169 | 170 | it('should not allow access to a database that was not allowed inside a `do`', (done) => { 171 | executeProxyQuery(r.expr(true).do(function () { 172 | return [r.db(secondDbName).tableList(), 4, 5, 9]; 173 | })) 174 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 175 | .nodeify(done); 176 | }); 177 | 178 | it('should allow access to a database that is allowed', (done) => { 179 | executeProxyQuery(r.db(dbName).tableList()) 180 | .then(function (list) { 181 | list.should.be.instanceof(Array); 182 | }) 183 | .nodeify(done); 184 | }); 185 | 186 | describe('Connection', () => { 187 | it('should throw an error when trying to connect with an unallowed database', (done) => { 188 | r.connect({ port: proxyPort, db: secondDbName }) 189 | .then(function (conn) { 190 | return r.tableList().run(conn); 191 | }) 192 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 193 | .nodeify(done); 194 | }); 195 | }); 196 | 197 | describe('Query arguments', () => { 198 | 199 | it('should not allow a database name to be passed through r.args', function (done) { 200 | // For some reason, using `executeProxyQuery` doesn't work... 201 | r.connect({ port: proxyPort }).then(function (conn) { 202 | return r.db(r.args([dbName])).tableList().run(conn) 203 | .then(conn.close.bind(conn)); 204 | }) 205 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 206 | .nodeify(done); 207 | }); 208 | 209 | it('should allow a database name to be passed through r.expr', (done) => { 210 | executeProxyQuery(r.db(r.expr(dbName)).tableList()) 211 | .then(function (list) { 212 | list.should.be.instanceof(Array); 213 | }) 214 | .nodeify(done); 215 | }); 216 | 217 | }); 218 | 219 | }); 220 | 221 | describe('Table Access', () => { 222 | 223 | before(function (done) { 224 | this.timeout(10000); 225 | createDatabase() 226 | .then(createSecondDatabase) 227 | .then(() => { 228 | return new Promise(function (resolve, reject) { 229 | server = new RethinkDBProxy({ 230 | port: proxyPort, 231 | dbs: [dbName, 'someOtherDb'], 232 | tables: dbName + '.' + tableName 233 | }); 234 | server.listen(resolve); 235 | }); 236 | }) 237 | .nodeify(done); 238 | }); 239 | 240 | after((done) => { 241 | dropDatabase() 242 | .then(server.close.bind(server)) 243 | .then(done.bind(null, null)); 244 | }); 245 | 246 | it('should not allow access to a table if not allowed', function (done) { 247 | this.timeout(5000); 248 | r.connect({ db: dbName }).then(function (conn) { 249 | return r.tableCreate('someOtherTable').run(conn) 250 | .then(function () { 251 | return conn.close(); 252 | }); 253 | }) 254 | .then(function () { 255 | return executeProxyQuery(r.db(dbName).table('someOtherTable')) 256 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)) 257 | .nodeify(done); 258 | }); 259 | }); 260 | 261 | it('should allow access to a table if allowed', (done) => { 262 | executeProxyQuery(r.db(dbName).table(tableName)) 263 | .then(function (list) { 264 | list.should.be.instanceof(Array); 265 | }) 266 | .nodeify(done); 267 | }); 268 | 269 | describe('Connection', () => { 270 | it('should not throw an error when trying query a table with an unallowed default database', (done) => { 271 | r.connect({ port: proxyPort, db: 'someOtherDbNotAllowed' }) 272 | .then(function (conn) { 273 | return r.table(tableName).run(conn); 274 | }) 275 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DATABASE/i)) 276 | .nodeify(done); 277 | }); 278 | 279 | it('should not throw an error when trying query a table with an unallowed table', (done) => { 280 | r.connect({ port: proxyPort, db: dbName }) 281 | .then(function (conn) { 282 | return r.table(tableName).coerceTo('array').run(conn); 283 | }) 284 | .then(function (list) { 285 | return list.should.be.instanceof(Array); 286 | }) 287 | .nodeify(done); 288 | }); 289 | 290 | it('should throw an error when trying query a table with an unallowed default database', (done) => { 291 | r.connect({ port: proxyPort, db: dbName }) 292 | .then(function (conn) { 293 | return r.table('someOtherTable').run(conn); 294 | }) 295 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)) 296 | .nodeify(done); 297 | }); 298 | }); 299 | 300 | describe('Query arguments', () => { 301 | 302 | it('should not allow a table name to be passed through r.args', (done) => { 303 | executeProxyQuery(r.db(dbName).table(r.args([tableName]))) 304 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)) 305 | .nodeify(done); 306 | }); 307 | 308 | it('should allow a table name to be passed through r.expr', (done) => { 309 | executeProxyQuery(r.db(dbName).table(r.expr(tableName))) 310 | .then(function (list) { 311 | list.should.be.instanceof(Array); 312 | }) 313 | .nodeify(done); 314 | }); 315 | 316 | it('should not allow a table name to be passed through a ReQL expression', (done) => { 317 | executeProxyQuery(r.db(dbName).table(r.db(dbName).tableList()(0))) 318 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /TABLE/i)) 319 | .nodeify(done); 320 | }); 321 | 322 | }); 323 | 324 | }); 325 | 326 | 327 | }); 328 | -------------------------------------------------------------------------------- /test/edge-cases.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | import { makeExecuteQuery, makeExecuteProxyQuery, makeAssertQuery, makeCreateDatabase, makeDropDatabase } from './utils'; 7 | import protoDef from '../node_modules/rethinkdb/proto-def'; 8 | 9 | let proxyPort = 8125; 10 | let dbName = 'rethinkdb_proxy_test'; 11 | let tableName = 'entries'; 12 | let server; 13 | 14 | let executeQuery = makeExecuteQuery(dbName, proxyPort); 15 | let executeProxyQuery = makeExecuteProxyQuery(dbName, proxyPort); 16 | let assertQuery = makeAssertQuery(executeQuery); 17 | let createDatabase = makeCreateDatabase(dbName, tableName); 18 | let dropDatabase = makeDropDatabase(dbName); 19 | let throwError = function (res) { throw new Error(); }; 20 | let expectError = function (errorName, errorMessageMatch, err) { 21 | if (errorName !== null) errorName.should.equal(err.name); 22 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 23 | (err instanceof Error).should.equal(true); 24 | }; 25 | 26 | describe('Edge Cases', () => { 27 | 28 | let get = r.db(dbName).table(tableName); 29 | 30 | describe('Replace', () => { 31 | 32 | before(function (done) { 33 | this.timeout(5000); 34 | createDatabase() 35 | .then(() => { 36 | return new Promise(function (resolve, reject) { 37 | server = new RethinkDBProxy({ 38 | port: proxyPort, 39 | allowReplace: true, 40 | }); 41 | server.listen(resolve); 42 | }); 43 | }) 44 | .then(() => { 45 | return r.connect().then((conn) => { 46 | return get.insert([{ 47 | id: 1, name: 'Hello' 48 | }, { 49 | id: 2, name: 'Jorge' 50 | }]).run(conn).then(() => { 51 | return conn.close(); 52 | }); 53 | }); 54 | }) 55 | .nodeify(done); 56 | }); 57 | 58 | after((done) => { 59 | dropDatabase() 60 | .then(server.close.bind(server)) 61 | .then(done.bind(null, null)); 62 | }); 63 | 64 | it('should not allow a query that passes null to `replace` if `delete` is not allowed', (done) => { 65 | executeQuery(get.get(1).replace(null)) 66 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /REPLACE/i)) 67 | .nodeify(done); 68 | }); 69 | 70 | it('should not allow a query that passes null to `replace` if `delete` is not allowed', (done) => { 71 | executeQuery(get.replace(null)) 72 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /REPLACE/i)) 73 | .nodeify(done); 74 | }); 75 | 76 | }); 77 | 78 | describe('Insert', () => { 79 | 80 | before(function (done) { 81 | this.timeout(5000); 82 | createDatabase() 83 | .then(() => { 84 | return new Promise(function (resolve, reject) { 85 | server = new RethinkDBProxy({ 86 | port: proxyPort, 87 | allowInsert: true, 88 | }); 89 | server.listen(resolve); 90 | }); 91 | }) 92 | .then(() => { 93 | return r.connect().then((conn) => { 94 | return get.insert([{ 95 | id: 1, name: 'Hello' 96 | }, { 97 | id: 2, name: 'Jorge' 98 | }]).run(conn).then(() => { 99 | return conn.close(); 100 | }); 101 | }); 102 | }) 103 | .nodeify(done); 104 | }); 105 | 106 | after((done) => { 107 | dropDatabase() 108 | .then(server.close.bind(server)) 109 | .then(done.bind(null, null)); 110 | }); 111 | 112 | it('should not allow a query that passes `conflict: replace` if `replace` is not allowed', (done) => { 113 | executeProxyQuery(get.insert({ id: 1, name: 'Hugo'}, { conflict: 'replace' })) 114 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /INSERT/i)) 115 | .nodeify(done); 116 | }); 117 | 118 | it('should not allow a query that passes `conflict: update` if `update` is not allowed', (done) => { 119 | executeProxyQuery(get.insert({ id: 1, name: 'Hugo'}, { conflict: 'update' })) 120 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /UPDATE/i)) 121 | .nodeify(done); 122 | }); 123 | 124 | }); 125 | 126 | }); 127 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | 3 | import './parser.js'; 4 | import './connection.js'; 5 | import './queries.js'; 6 | import './read-only-queries.js'; 7 | import './edge-cases.js'; 8 | import './database-and-table-access.js'; 9 | import './changefeeds.js'; 10 | import './parallel.js'; 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/parallel.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | import { makeExecuteQuery, makeExecuteProxyQuery, makeAssertQuery, makeCreateDatabase, makeDropDatabase } from './utils'; 7 | import protoDef from '../node_modules/rethinkdb/proto-def'; 8 | 9 | let proxyPort = 8125; 10 | let dbName = 'rethinkdb_proxy_test'; 11 | let secondDbName = 'rethinkdb_proxy_test_2'; 12 | let tableName = 'entries'; 13 | let server; 14 | 15 | let executeQuery = makeExecuteQuery(dbName, proxyPort); 16 | let executeProxyQuery = makeExecuteProxyQuery(dbName, proxyPort); 17 | let assertQuery = makeAssertQuery(executeQuery); 18 | let createDatabase = makeCreateDatabase(dbName, tableName); 19 | let createSecondDatabase = makeCreateDatabase(secondDbName, tableName); 20 | let dropDatabase = makeDropDatabase(dbName); 21 | let throwError = function (res) { throw new Error(); }; 22 | let expectError = function (errorName, errorMessageMatch, err) { 23 | if (errorName !== null) errorName.should.equal(err.name); 24 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 25 | (err instanceof Error).should.equal(true); 26 | }; 27 | 28 | 29 | describe('Parallel Queries', () => { 30 | 31 | let testData = [ 32 | { name: 'Germany' }, 33 | { name: 'Bhutan' }, 34 | { name: 'Maldives' }, 35 | ]; 36 | 37 | before(function (done) { 38 | this.timeout(10000); 39 | createDatabase() 40 | .then(() => { 41 | return r.connect().then((conn) => { 42 | return r.db(dbName).table(tableName) 43 | .insert(testData) 44 | .run(conn); 45 | }); 46 | }) 47 | .then(() => { 48 | return new Promise(function (resolve, reject) { 49 | server = new RethinkDBProxy({ 50 | port: proxyPort, 51 | }); 52 | server.listen(resolve); 53 | }); 54 | }) 55 | .nodeify(done); 56 | }); 57 | 58 | after((done) => { 59 | dropDatabase() 60 | .then(server.close.bind(server)) 61 | .then(done.bind(null, null)); 62 | }); 63 | 64 | it('should handle parallel connections', (done) => { 65 | //this.timeout(15000); 66 | let query = function (countryName) { 67 | return r.db(dbName).table(tableName) 68 | .filter({ 'name': countryName }) 69 | .coerceTo('array'); 70 | }; 71 | let query1 = query('Germany'); 72 | let query2 = query('Maldives'); 73 | let arr = (() => { 74 | let arr = []; 75 | for (let i = 0; i < 5; i += 1) arr.push(i); 76 | return arr; 77 | }()); 78 | r.connect({ port: proxyPort }).then((conn1) => { 79 | return Promise.all( 80 | [].concat(arr.map(() => query1.run(conn1) )) 81 | .concat(arr.map(() => query2.run(conn1) )) 82 | ) 83 | .then((res) => { 84 | res.slice(0, 5).forEach(row => row[0].name.should.equal('Germany')); 85 | res.slice(5, 10).forEach(row => row[0].name.should.equal('Maldives')); 86 | }); 87 | }) 88 | .nodeify(done); 89 | }); 90 | 91 | it('should handle parallel cursors', (done) => { 92 | r.connect({ port: proxyPort }).then((conn1) => { 93 | return r.db(dbName).table(tableName).run(conn1) 94 | .then((cursor) => { 95 | let processRow = (err, row) => { 96 | if (err === null) return cursor.next(processRow); 97 | if (err.msg === 'No more rows in the cursor.') return done(); 98 | return done(err); 99 | }; 100 | cursor.next(processRow); 101 | }); 102 | }); 103 | }); 104 | 105 | }); 106 | 107 | 108 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | import BufferParser from './components/buffer-parser'; 2 | import protoDef from '../node_modules/rethinkdb/proto-def'; 3 | import 'should'; 4 | 5 | describe('Buffer Parser', () => { 6 | 7 | let parser; 8 | let parseQuery = (...args) => { 9 | let result = []; 10 | let cb = args.pop(); 11 | parser.on('query', (query) => { 12 | result.push(query); 13 | }); 14 | let count = 0; 15 | for(let key in args) { 16 | if (args.hasOwnProperty(key)) { 17 | let value = args[key]; 18 | let token = count++; 19 | let tokenBuffer = new Buffer(8); 20 | tokenBuffer.writeUInt32LE(Math.floor(token / 0xFFFFFFFF), 4); 21 | let byteLengthBuffer = new Buffer(4); 22 | let queryBuffer = new Buffer(JSON.stringify(value)); 23 | byteLengthBuffer.writeUInt32LE(queryBuffer.length, 0); 24 | parser.append(Buffer.concat([tokenBuffer, byteLengthBuffer, queryBuffer])); 25 | } 26 | } 27 | cb(result); 28 | }; 29 | 30 | beforeEach(() => { 31 | parser = new BufferParser(); 32 | let version = new Buffer(4); 33 | version.writeUInt32LE(protoDef.VersionDummy.Version.VO_4, 0); 34 | let auth_buffer = new Buffer('', 'ascii'); 35 | let auth_length = new Buffer(4); 36 | auth_length.writeUInt32LE(auth_buffer.length, 0); 37 | let protocol = new Buffer(4); 38 | protocol.writeUInt32LE(protoDef.VersionDummy.Protocol.JSON, 0); 39 | var token = Buffer.concat([version, auth_length, auth_buffer, protocol]); 40 | parser.append(token); 41 | }); 42 | 43 | 44 | it('should correctly parse a single buffer', () => { 45 | let value = [[1]]; 46 | parseQuery(value, (result) => { 47 | result[0].should.eql(value); 48 | }); 49 | }); 50 | 51 | it('should correctly parse a multiples buffers', () => { 52 | let value0 = [[1]]; 53 | let value1 = [1, [3, [4, [3]]]]; 54 | let value2 = [1, [3, [4, [3]]]]; 55 | parseQuery(value0, value1, value2, (result) => { 56 | result[0].should.eql(value0); 57 | result[1].should.eql(value1); 58 | result[2].should.eql(value2); 59 | }); 60 | 61 | }); 62 | 63 | it('should correctly parse a parser with `]` inside the bytelength/token`', () => { 64 | let value = [1,[51,[[39,[[15,[[14,['rethinkdb_proxy_test']],'entries']],{'name':'Germany'}]],'array']]]; 65 | parseQuery(value, value, value, (result) => { 66 | result[0].should.eql(value); 67 | result[1].should.eql(value); 68 | result[2].should.eql(value); 69 | }); 70 | 71 | }); 72 | 73 | it('should correctly handle strings with `[]`', () => { 74 | let value = [1,[51,[[39,[[15,[[14,['rethinkdb_proxy_test']],'entries']],{'name':'[[[[[[['}]],'array']]]; 75 | parseQuery(value, value, value, (result) => { 76 | result[0].should.eql(value); 77 | result[1].should.eql(value); 78 | result[2].should.eql(value); 79 | }); 80 | }); 81 | 82 | }); 83 | -------------------------------------------------------------------------------- /test/queries.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | import { makeExecuteQuery, makeAssertQuery, makeCreateDatabase, makeDropDatabase } from './utils'; 7 | 8 | let proxyPort = 8125; 9 | let dbName = 'rethinkdb_proxy_test'; 10 | let tableName = 'entries'; 11 | let server; 12 | 13 | let executeQuery = makeExecuteQuery(dbName, proxyPort); 14 | let assertQuery = makeAssertQuery(executeQuery); 15 | let createDatabase = makeCreateDatabase(dbName, tableName); 16 | let dropDatabase = makeDropDatabase(dbName); 17 | 18 | describe('Normal Queries', () => { 19 | 20 | before(function (done) { 21 | this.timeout(5000); 22 | createDatabase() 23 | .then(() => { 24 | server = new RethinkDBProxy({ 25 | port: proxyPort, 26 | allowWrites: true 27 | }); 28 | server.listen(done); 29 | }); 30 | }); 31 | 32 | after((done) => { 33 | server.close().then(dropDatabase) 34 | .then(done.bind(null, null)); 35 | }); 36 | 37 | describe('Read Queries', () => { 38 | 39 | it('should return an `r.expr` successfully', (done) => { 40 | assertQuery(r.expr([1, 2, 3])) 41 | .nodeify(done); 42 | }); 43 | 44 | it('should return the same list of databases', (done) => { 45 | assertQuery(r.dbList()) 46 | .nodeify(done); 47 | }); 48 | 49 | it('should handle group queries', (done) => { 50 | assertQuery( 51 | r.expr([{'v': 1}, {'v': 2}, {'v' : 2}, {'v' : 4}]) 52 | .group('v').count().ungroup() 53 | ) 54 | .nodeify(done); 55 | }); 56 | 57 | }); 58 | 59 | describe('Write Queries', () => { 60 | 61 | it('should return the same result after a write', (done) => { 62 | let get = r.db(dbName).table(tableName); 63 | executeQuery(get.insert({ hello: 'world'})) 64 | .then(() => { 65 | return assertQuery(get.orderBy('id')); 66 | }) 67 | .then(() => { 68 | return r.connect().then((conn) => { 69 | return get.count().run(conn) 70 | .then((count) => { 71 | //count.should.eql(2); 72 | }); 73 | }); 74 | }) 75 | .nodeify(done); 76 | }); 77 | 78 | }); 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /test/read-only-queries.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import r from 'rethinkdb'; 3 | import Promise from 'bluebird'; 4 | import RethinkDBProxy from './components/rethinkdb-proxy'; 5 | import should from 'should'; 6 | import { makeExecuteQuery, makeExecuteProxyQuery, makeAssertQuery, makeCreateDatabase, makeDropDatabase } from './utils'; 7 | import protoDef from '../node_modules/rethinkdb/proto-def'; 8 | 9 | let proxyPort = 8125; 10 | let dbName = 'rethinkdb_proxy_test'; 11 | let tableName = 'entries'; 12 | let server; 13 | 14 | let executeQuery = makeExecuteQuery(dbName, proxyPort); 15 | let executeProxyQuery = makeExecuteProxyQuery(dbName, proxyPort); 16 | let assertQuery = makeAssertQuery(executeQuery); 17 | let createDatabase = makeCreateDatabase(dbName, tableName); 18 | let dropDatabase = makeDropDatabase(dbName); 19 | let throwError = function (res) { throw new Error(); }; 20 | let expectError = function (errorName, errorMessageMatch, err) { 21 | if (errorMessageMatch !== null) err.msg.should.match(errorMessageMatch); 22 | if (errorName !== null) errorName.should.equal(err.name); 23 | (err instanceof Error).should.equal(true); 24 | }; 25 | 26 | describe('Unallowed Queries', () => { 27 | let get = r.db(dbName).table(tableName); 28 | 29 | before(function (done) { 30 | this.timeout(5000); 31 | createDatabase() 32 | .then(() => { 33 | server = new RethinkDBProxy({ 34 | port: proxyPort, 35 | }); 36 | return server.listen().then(done); 37 | }); 38 | }); 39 | 40 | after((done) => { 41 | dropDatabase() 42 | .then(server.close.bind(server)) 43 | .then(done.bind(null, null)); 44 | }); 45 | 46 | describe('Read Queries', () => { 47 | 48 | it('should return an `r.expr` successfully', (done) => { 49 | assertQuery(r.expr([1, 2, 3])) 50 | .nodeify(done); 51 | }); 52 | 53 | it('should return the same list of databases', (done) => { 54 | assertQuery(r.dbList()) 55 | .nodeify(done); 56 | }); 57 | 58 | it('should allow for queries inside arrays', (done) => { 59 | assertQuery(r.expr([ get.coerceTo('array'), get.coerceTo('array') ])) 60 | .nodeify(done); 61 | }); 62 | 63 | }); 64 | 65 | describe('Insert', function () { 66 | 67 | it('should throw an error after attempting to write to the database', (done) => { 68 | executeProxyQuery(get.insert({ hello: 'world' })) 69 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /INSERT/i)) 70 | .nodeify(done); 71 | }); 72 | 73 | it('should not throw an error when using the same number as the `write` proto buff definition', (done) => { 74 | executeProxyQuery(r.expr([protoDef.Term.TermType.INSERT, 1, 2, 3])) 75 | .then(function (res) { 76 | res.should.eql([protoDef.Term.TermType.INSERT, 1, 2, 3]); 77 | }) 78 | .nodeify(done); 79 | }); 80 | 81 | it('should not allow insert queries inside `do`', (done) => { 82 | executeProxyQuery(r.expr([1, 2, 3]).do(function (row) { return get.insert({ name: row }); })) 83 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /INSERT/i)) 84 | .nodeify(done); 85 | }); 86 | 87 | 88 | it('should not allow for write queries inside arrays', (done) => { 89 | executeProxyQuery(r.expr([ get.delete(), get.coerceTo('array') ])) 90 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DELETE/i)) 91 | .nodeify(done); 92 | }); 93 | }); 94 | 95 | describe('Delete', () => { 96 | it('should not allow delete queries inside `forEach` inside a `do`', (done) => { 97 | executeProxyQuery(get.coerceTo('array').do((rows) => { 98 | return rows.forEach((row) => { 99 | return get.get(row('id')).delete(); 100 | }); 101 | })) 102 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /DELETE/i)) 103 | .nodeify(done); 104 | }); 105 | }); 106 | 107 | describe('HTTP', function () { 108 | 109 | it('should not allow http queries inside `map` inside a `do`', (done) => { 110 | executeProxyQuery(get.coerceTo('array').slice(0, 3).do((rows) => { 111 | return rows.map((row) => { 112 | return r.http('http://www.reddit.com/r/javascript.json'); 113 | }); 114 | })) 115 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /HTTP/i)) 116 | .nodeify(done); 117 | }); 118 | 119 | it('should not allow http queries inside `do`', (done) => { 120 | executeProxyQuery(r.expr('hello').do((rows) => { 121 | return r.http('http://www.reddit.com/r/javascript.json'); 122 | })) 123 | .then(throwError, expectError.bind(null, 'ReqlDriverError', /HTTP/i)) 124 | .nodeify(done); 125 | }); 126 | 127 | }); 128 | 129 | }); 130 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext:true */ 2 | import Promise from 'bluebird'; 3 | import r from 'rethinkdb'; 4 | 5 | export function makeExecuteQuery (dbName, proxyPort) { 6 | return function (query) { 7 | return Promise.resolve() 8 | .then(() => { 9 | return [r.connect({ db: dbName }), r.connect({ port: proxyPort, db: dbName })]; 10 | }) 11 | .spread((connA, connB) => { 12 | return Promise.all([query.run(connA), query.run(connB)]) 13 | .spread((resultA, resultB) => { 14 | if (typeof resultA.toArray === 'function' && typeof resultA.each === 'function') { 15 | return Promise.all([ 16 | resultA.toArray(), 17 | resultB.toArray() 18 | ]); 19 | } 20 | return [resultA, resultB]; 21 | }) 22 | .finally(function () { 23 | return Promise.all([connA.close(), connB.close()]); 24 | }); 25 | }); 26 | }; 27 | } 28 | 29 | export function makeExecuteProxyQuery (dbName, proxyPort) { 30 | return function (query) { 31 | return Promise.resolve() 32 | .then(() => { 33 | return r.connect({ port: proxyPort, db: dbName }); 34 | }) 35 | .then((conn) => { 36 | return query.run(conn) 37 | .then((result) => { 38 | if (typeof result.toArray === 'function' && typeof result.each === 'function') { 39 | return result.toArray(); 40 | } 41 | return result; 42 | }) 43 | .finally(function () { 44 | return conn.close(); 45 | }); 46 | }); 47 | }; 48 | } 49 | 50 | export function makeAssertQuery (executeQuery) { 51 | return function (query) { 52 | return executeQuery(query) 53 | .spread((resultA, resultB) => { 54 | return resultA.should.eql(resultB); 55 | }); 56 | }; 57 | } 58 | 59 | export function makeCreateDatabase (dbName, tableName) { 60 | return function (done) { 61 | return r.connect().then((conn) => { 62 | return Promise.resolve() 63 | .then(() => { 64 | return r.dbCreate(dbName).run(conn) 65 | .catch((err) => { }); 66 | }) 67 | .then(() => { 68 | return r.db(dbName).tableCreate(tableName).run(conn) 69 | .catch((err) => { }); 70 | }); 71 | }); 72 | }; 73 | } 74 | 75 | export function makeDropDatabase (dbName) { 76 | return function () { 77 | return r.connect().then((conn) => { 78 | return r.dbDrop(dbName).run(conn) 79 | .finally(() => conn.close()); 80 | }); 81 | }; 82 | } 83 | 84 | --------------------------------------------------------------------------------