├── test ├── tools │ ├── connection-params.js │ ├── test-helper.js │ └── create-test-table.js ├── query-parseSpec.js ├── filter-creationSpec.js └── happy-pathSpec.js ├── release.sh ├── test-run.js ├── .travis.yml ├── LICENSE ├── .gitignore ├── package.json ├── CHANGELOG.md ├── README.md └── src ├── query-parser.js ├── search.js └── provider.js /test/tools/connection-params.js: -------------------------------------------------------------------------------- 1 | exports.rethinkdb = { 2 | host: process.env.RETHINKDB_HOST || 'localhost', 3 | port: 28015, 4 | db: 'search_provider_test' 5 | }; 6 | exports.primaryKey = 'ds_id'; 7 | exports.testTable = 'test'; 8 | exports.deepstreamUrl = 'localhost:7071'; 9 | exports.deepstreamCredentials = { username: 'rethinkdb-search-provider' }; 10 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | if [ -z $1 ]; then 2 | echo "Please provide a release version: patch, minor or major" 3 | exit 4 | fi 5 | 6 | if [ $( npm whoami ) != "deepstreamio" ]; then 7 | echo "Please verify you can log into npm as deepstreamio before trying to release" 8 | exit 9 | fi 10 | 11 | echo 'Starting release' 12 | npm version $1 13 | 14 | echo 'Pushing to github' 15 | git push --follow-tags 16 | 17 | -------------------------------------------------------------------------------- /test-run.js: -------------------------------------------------------------------------------- 1 | const Provider = require( './src/provider' ) 2 | const provider = new Provider({ 3 | logLevel: 0, 4 | // deepstream 5 | deepstreamUrl: 'localhost:6021', 6 | deepstreamCredentials: { username: 'rethinkdb-search-provider' }, 7 | 8 | // rethinkdb 9 | rethinkdbConnectionParams: { 10 | host: '192.168.56.101', 11 | port: 28015, 12 | db: 'deepstream' 13 | } 14 | }) 15 | 16 | provider.start(); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - CXX=g++-4.8 4 | 5 | addons: 6 | rethinkdb: '2.3' 7 | 8 | # Do not insert any code under here without making sures it's in publishingtest first 9 | language: node_js 10 | 11 | plugins: 12 | apt: 13 | sources: 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - g++-4.8 17 | 18 | node_js: 19 | - "6" 20 | - "4" 21 | 22 | script: 23 | - npm run coverage 24 | 25 | after_script: 26 | - "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 deepstreamHub GmbH 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deepstream.io-provider-search-rethinkdb", 3 | "version": "2.0.0", 4 | "description": "Realtime searches using rethinkdb", 5 | "main": "src/provider.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "watch": "mocha -w", 9 | "coverage": "istanbul cover node_modules/mocha/bin/_mocha", 10 | "lint": "eslint \"src/**/*.js\" \"test/**/*.js\"", 11 | "ci": "npm run coverage && npm run lint" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb.git" 16 | }, 17 | "keywords": [ 18 | "deepstream", 19 | "search", 20 | "rethinkdb", 21 | "realtime", 22 | "streaming" 23 | ], 24 | "author": "deepstreamHub GmbH", 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issues" 28 | }, 29 | "homepage": "https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb", 30 | "dependencies": { 31 | "deepstream.io-client-js": "^2.0.0", 32 | "rethinkdb": "^2.3.3" 33 | }, 34 | "devDependencies": { 35 | "chai": "^3.5.0", 36 | "coveralls": "^2.11.9", 37 | "deepstream.io": "latest", 38 | "deepstream.io-storage-rethinkdb": "latest", 39 | "eslint": "^3.0.1", 40 | "istanbul": "^0.4.3", 41 | "mocha": "^3.0.0", 42 | "sinon": "^2.2.0", 43 | "sinon-chai": "^2.8.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/tools/test-helper.js: -------------------------------------------------------------------------------- 1 | var createTestTable = require( './create-test-table' ), 2 | connectionParams = require( './connection-params' ), 3 | Provider = require( '../../src/provider' ), 4 | DeepstreamClient = require( 'deepstream.io-client-js' ), 5 | ds; 6 | 7 | 8 | exports.createTestTable = function( done ) { 9 | createTestTable( done ); 10 | }; 11 | 12 | exports.startProvider = function( done ) { 13 | var provider = new Provider({ 14 | logLevel: 0, 15 | listName: 'search', 16 | deepstreamUrl: connectionParams.deepstreamUrl, 17 | deepstreamCredentials: connectionParams.deepstreamCredentials, 18 | rethinkdbConnectionParams: connectionParams.rethinkdb 19 | }); 20 | 21 | provider.on( 'ready', function(){ 22 | done( provider ); 23 | }); 24 | 25 | provider.start(); 26 | }; 27 | 28 | exports.connectToDeepstream = function( done ) { 29 | var ds = new DeepstreamClient( connectionParams.deepstreamUrl ); 30 | ds.on('error', function(message) { 31 | console.log( 'ERROR on client', message ); 32 | }) 33 | ds.login( { username: 'testClient' }, function( success ){ 34 | if( !success ) { 35 | done(new Error( 'Could not connect' )); 36 | } else { 37 | done( null, ds ); 38 | } 39 | }); 40 | }; 41 | 42 | exports.cleanUp = function( provider, deepstream, done ) { 43 | // FIXME: this is to wait for an unsubscription ACK - remove timeout when DS closes cleanly 44 | setTimeout( () => { 45 | provider.stop(); 46 | deepstream.close(); 47 | done(); 48 | }, 100 ); 49 | }; 50 | -------------------------------------------------------------------------------- /test/tools/create-test-table.js: -------------------------------------------------------------------------------- 1 | var r = require( 'rethinkdb' ), 2 | connectionParams = require( './connection-params' ), 3 | connection, 4 | callback; 5 | 6 | module.exports = function( _callback ) { 7 | callback = _callback; 8 | r.connect( connectionParams.rethinkdb, function( error, _connection ){ 9 | if( error ) { 10 | return _callback(error) 11 | } 12 | 13 | connection = _connection; 14 | r.dbDrop( connectionParams.rethinkdb.db ).run( connection, function(err) { 15 | if (err) { 16 | // do nothing, this just happens the first time 17 | } 18 | r.dbCreate( connectionParams.rethinkdb.db ).run(connection, function(err) { 19 | if (err) { 20 | return _callback(err) 21 | } 22 | createTable() 23 | }) 24 | } ); 25 | }); 26 | }; 27 | 28 | var createTable = function() { 29 | r 30 | .tableCreate( connectionParams.testTable, { 31 | primaryKey: connectionParams.primaryKey, 32 | durability: 'hard' 33 | } ) 34 | .run( connection, fn( populateTable ) ); 35 | }; 36 | 37 | var populateTable = function( error ) { 38 | r.table( connectionParams.testTable ).insert([ 39 | { ds_id: 'don', __ds: { _v: 1 }, title: 'Don Quixote', author: 'Miguel de Cervantes', language: 'Spanish', released: 1605, copiesSold: 315000000 }, 40 | { ds_id: 'tct', __ds: { _v: 1 }, title: 'A Tale Of Two Cities', author: 'Charles Dickens', language: 'English', released: 1859, copiesSold: 200000000 }, 41 | { ds_id: 'lor', __ds: { _v: 1 }, title: 'The Lord of the Rings', author: 'J. R. R. Tolkien', language: 'English', released: 1954, copiesSold: 150000000 }, 42 | { ds_id: 'tlp', __ds: { _v: 1 }, title: 'The Little Prince', author: 'Antoine de Saint-Exupéry', language: 'French', released: 1943, copiesSold: 140000000 }, 43 | { ds_id: 'hrp', __ds: { _v: 1 }, title: 'Harry Potter and the Philosopher\'s Stone', author: 'J. K. Rowling', language: 'English', released: 1997, copiesSold: 107000000 } 44 | ]).run( connection, fn( addIndex ) ); 45 | }; 46 | 47 | var addIndex = function( error ) { 48 | r.table( connectionParams.testTable ).indexCreate( 'released' ).run( connection, fn( complete ) ) 49 | } 50 | 51 | var complete = function() { 52 | connection.close(); 53 | callback(); 54 | }; 55 | 56 | var fn = function( cb ) { 57 | return function( error ) { 58 | if( error ) { 59 | cb(err) 60 | } else { 61 | cb(); 62 | } 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.0.0] - 2016-12-5 2 | 3 | ### Enhancements 4 | - Queries now support order and limit [#1](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issues/1) by [@Iiridayn](https://github.com/Iiridayn) 5 | 6 | ### Breaking change 7 | - Now using v2.0.0 of the deepstream client, this is not compatible with v1 of the server 8 | 9 | ## [1.2.0] - 2016-10-06 10 | 11 | ### Enhancements 12 | - New config parameter primaryKey [#42](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issues/42) 13 | - Queries now support nested paths [#44](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issues/44) 14 | - Queries now support `ge` and `le` [#38](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issues/38) by [@Iiridayn](https://github.com/Iiridayn) 15 | - Queries now support `Date` [#37](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issues/37) by [@Iiridayn](https://github.com/Iiridayn) 16 | 17 | ## [1.1.2] - 2016-09-11 18 | 19 | ### Enhancement 20 | 21 | - Now uses Advanced listening ( v1.1 of the client ) 22 | 23 | ### Bug Fixes 24 | 25 | - Fix query string parsing [#6](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/pull/6) by [@alongubkin](https://github.com/alongubkin) 26 | - Subscribing and then deleting crashes server [#22](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issue/22), [PR #25](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/pull/25) by [@Iiridayn](https://github.com/Iiridayn) 27 | - Cannot get all records on a table with an empty query [#23](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issue/23), [PR #25](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/pull/25) by [@Iiridayn](https://github.com/Iiridayn) 28 | - Workaround for list record reference until subscription feature is fixed in deepstream [#24](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/issue/24), [PR #25](https://github.com/deepstreamIO/deepstream.io-provider-search-rethinkdb/pull/25) by [@Iiridayn](https://github.com/Iiridayn) 29 | 30 | ## [1.1.1] - 2016-07-12 31 | 32 | ### Enhancement 33 | 34 | - Now using v1.0.0 of the deepstream client 35 | 36 | ## [1.1.0] - 2016-07-5 37 | 38 | ### Enhancement 39 | 40 | - Catch errors by subscribing to all error events 41 | -------------------------------------------------------------------------------- /test/query-parseSpec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const sinon = require( 'sinon' ) 3 | const sinonChai = require("sinon-chai") 4 | require('chai').use(sinonChai) 5 | 6 | const QueryParser = require( '../src/query-parser' ) 7 | const log = sinon.spy() 8 | const queryParser = new QueryParser({ log: (log) }) 9 | 10 | describe( 'the query parser', () => { 11 | 12 | it( 'parses queries with a single condition correctly', () => { 13 | var query = { 14 | table: 'books', 15 | query: [[ 'name', 'eq', 'Harry Potter' ]] 16 | } 17 | var parsedInput = queryParser.parseInput( 'search?' + JSON.stringify(query) ) 18 | 19 | expect( parsedInput ).to.deep.equal( query ) 20 | expect( log ).to.have.not.been.called 21 | }) 22 | 23 | it( 'parses queries with multiple conditions correctly', () => { 24 | var query = { 25 | table: 'books', 26 | query: [ 27 | [ 'name', 'eq', 'Harry Potter' ], 28 | [ 'price', 'gt', 12.3 ], 29 | [ 'publisher', 'match', '.*random.*' ] 30 | ] 31 | } 32 | var parsedInput = queryParser.parseInput( 'search?' + JSON.stringify(query) ) 33 | 34 | expect( parsedInput ).to.deep.equal( query ) 35 | expect( log ).to.have.not.been.called 36 | }) 37 | 38 | it( 'errors for missing question marks', () => { 39 | var parsedInput = queryParser.parseInput( 'search{"table":"books","query":[["name","eq","Harry Potter"]]}' ) 40 | expect( parsedInput ).to.equal( null ) 41 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Missing ?', 1 ) 42 | }) 43 | 44 | it( 'errors for invalid JSON', () => { 45 | var parsedInput = queryParser.parseInput( 'search?{"table":"books""query":[["name","eq","Harry Potter"]]}' ) 46 | expect( parsedInput ).to.equal( null ) 47 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Invalid JSON', 1 ) 48 | }) 49 | 50 | it( 'errors for missing table parameter', () => { 51 | var parsedInput = queryParser.parseInput( 'search?{"query":[["name","eq","Harry Potter"]]}' ) 52 | expect( parsedInput ).to.equal( null ) 53 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Missing parameter "table"', 1 ) 54 | }) 55 | 56 | it( 'errors for missing query parameter', () => { 57 | var parsedInput = queryParser.parseInput( 'search?{"table":"books"}' ) 58 | expect( parsedInput ).to.equal( null ) 59 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Missing parameter "query"', 1 ) 60 | }) 61 | 62 | it( 'errors for order without limit', () => { 63 | var parsedInput = queryParser.parseInput( 'search?{"table":"books","query":[],"order":"price"}' ) 64 | expect( parsedInput ).to.equal( null ) 65 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Must specify both "order" and "limit" together', 1 ) 66 | }) 67 | 68 | it( 'errors for limit without order', () => { 69 | var parsedInput = queryParser.parseInput( 'search?{"table":"books","query":[],"limit":1}' ) 70 | expect( parsedInput ).to.equal( null ) 71 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Must specify both "order" and "limit" together', 1 ) 72 | }) 73 | 74 | it( 'errors for malformed query', () => { 75 | var parsedInput = queryParser.parseInput( 'search?{"table":"books","query":[["eq","Harry Potter"]]}' ) 76 | expect( parsedInput ).to.equal( null ) 77 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Too few parameters', 1 ) 78 | }) 79 | 80 | it( 'errors for unknown operator', () => { 81 | var parsedInput = queryParser.parseInput( 'search?{"table":"books","query":[["name","ex","Harry Potter"]]}' ) 82 | expect( parsedInput ).to.equal( null ) 83 | expect( log ).to.have.been.calledWith( 'QUERY ERROR | Unknown operator ex', 1 ) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /test/filter-creationSpec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | const QueryParser = require( '../src/query-parser' ) 3 | const queryParser = new QueryParser({ primaryKey: 'ds_id', log: () => {} }) 4 | 5 | function getFilter( queryJson ) { 6 | var searchString = 'search?' + JSON.stringify( queryJson ) 7 | var query = queryParser.createQuery( queryParser.parseInput( searchString ) ) 8 | return query.toString() 9 | } 10 | 11 | describe( 'the query builder', () => { 12 | 13 | it( 'creates the right filter for a query with no conditions', () => { 14 | expect( getFilter({ table: 'someTable', query: [] }) ).to.equal( 'r.table("someTable")("ds_id")' ); 15 | }) 16 | 17 | it( 'creates the right filter for a query with one condition', () => { 18 | var filterString = getFilter({ 19 | table: 'someTable', 20 | query: [[ 'title', 'eq', 'Don Quixote' ] ] 21 | }) 22 | expect( filterString ).to.equal( 'r.table("someTable").filter(r.row("title").eq("Don Quixote"))("ds_id")' ) 23 | }) 24 | 25 | 26 | it( 'creates a filter for a query for nested fields', () => { 27 | var filterString = getFilter({ 28 | table: 'someTable', 29 | query: [[ 'features.frontdoor', 'eq', 'Don Quixote' ] ] 30 | }) 31 | expect( filterString ).to.equal( 'r.table("someTable").filter(r.row("features")("frontdoor").eq("Don Quixote"))("ds_id")' ) 32 | }) 33 | 34 | it( 'creates a filter for a query with deeply nested fields', () => { 35 | var filterString = getFilter({ 36 | table: 'someTable', 37 | query: [[ 'a.c[2].e', 'eq', 'Don Quixote' ] ] 38 | }) 39 | expect( filterString ).to.equal( 'r.table("someTable").filter(r.row("a")("c")("2")("e").eq("Don Quixote"))("ds_id")' ) 40 | }) 41 | 42 | it( 'creates the right filter for a query with a question mark', () => { 43 | var filterString = getFilter({ 44 | table: 'someTable', 45 | query: [[ 'artist', 'eq', '? and the Mysterians' ] ] 46 | }) 47 | expect( filterString ).to.equal( 'r.table("someTable").filter(r.row("artist").eq("? and the Mysterians"))("ds_id")' ) 48 | 49 | }) 50 | 51 | it( 'creates the right filter for a query with multiple conditions', () => { 52 | var filterString = getFilter({ 53 | table: 'someTable', 54 | query: [ 55 | [ 'title', 'eq', 'Don Quixote' ], 56 | [ 'released', 'gt', 1700 ], 57 | [ 'author', 'match', '.*eg' ] 58 | ] 59 | }) 60 | 61 | expect( filterString ).to.equal( 62 | 'r.table("someTable").filter(r.row("title").eq("Don Quixote")).filter(r.row("released").gt(1700)).filter(r.row("author").match(".*eg"))("ds_id")' 63 | ) 64 | }) 65 | 66 | it( 'creates the right filter for a query with ge/le', () => { 67 | var filterString = getFilter({ 68 | table: 'someTable', 69 | query: [ 70 | [ 'released', 'ge', 1700 ], 71 | [ 'released', 'le', 1800 ], 72 | ] 73 | }) 74 | 75 | expect( filterString ).to.equal( 76 | 'r.table("someTable").filter(r.row("released").ge(1700)).filter(r.row("released").le(1800))("ds_id")' 77 | ) 78 | }) 79 | 80 | it( 'creates the right filter for a query with in', () => { 81 | var filterString = getFilter({ 82 | table: 'someTable', 83 | query: [[ 'released', 'in', [1706, 1708, 1869] ]] 84 | }) 85 | expect( filterString ).to.match( /^r.table\("someTable"\)\.filter\(function\(var_(\d+)\) { return r\(\[1706, 1708, 1869\]\)\.contains\(var_\1\("released"\)\); }\)\("ds_id"\)$/ ) 86 | }) 87 | 88 | /* 89 | it( 'creates the right filter for a query with nested fields and in', () => { 90 | var filterString = getFilter({ 91 | table: 'someTable', 92 | query: [[ 'dates.released', 'in', [1706, 1708, 1869] ]] 93 | }) 94 | expect( filterString ).to.match( /^r.table\("someTable"\)\.filter\(function\(var_(\d+)\) { return r\(\[1706, 1708, 1869\]\)\.contains\(var_\1\("dates"\)\("released"\)\); }\)\("ds_id"\)$/ ) 95 | }) 96 | */ 97 | 98 | it( 'creates the right filter for a query with order and limit', () => { 99 | var filterString = getFilter({ 100 | table: 'books', 101 | query: [ 102 | [ 'released', 'gt', 1700 ] 103 | ], 104 | order: 'released', 105 | desc: true, 106 | limit: 1 107 | }) 108 | expect( filterString ).to.equal( 'r.table("books").orderBy({"index": r.desc("released")}).filter(r.row("released").gt(1700))("ds_id").limit(1)' ) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UNMAINTAINED 2 | 3 | # deepstream.io-provider-search-rethinkdb 4 | 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/deepstreamIO/deepstream.io-provider-search-rethinkdb.svg)](https://greenkeeper.io/) 6 | 7 | [![Build Status](https://travis-ci.org/deepstreamIO/deepstream.io-provider-search-rethinkdb.svg?branch=master)](https://travis-ci.org/deepstreamIO/deepstream.io-provider-search-rethinkdb) 8 | [![Coverage Status](https://coveralls.io/repos/github/deepstreamIO/deepstream.io-provider-search-rethinkdb/badge.svg?branch=master)](https://coveralls.io/github/deepstreamIO/deepstream.io-provider-search-rethinkdb?branch=master) 9 | [![npm](https://img.shields.io/npm/v/deepstream.io-provider-search-rethinkdb.svg)](https://www.npmjs.com/package/deepstream.io-provider-search-rethinkdb) 10 | [![Dependency Status](https://david-dm.org/deepstreamIO/deepstream.io-provider-search-rethinkdb.svg)](https://david-dm.org/deepstreamIO/deepstream.io-provider-search-rethinkdb) 11 | [![devDependency Status](https://david-dm.org/deepstreamIO/deepstream.io-provider-search-rethinkdb/dev-status.svg)](https://david-dm.org/deepstreamIO/deepstream.io-provider-search-rethinkdb#info=devDependencies) 12 | 13 | 14 | Adds realtime search functionality to deepstream when used in conjunction with RethinkDb. 15 | 16 | Say you've got a number of records like: 17 | 18 | ```js 19 | ds.record.getRecord( 'book/i95ny80q-2bph9txxqxg' ).set({ 20 | 'title': 'Harry Potter and the goblet of fire', 21 | 'price': 9.99 22 | }); 23 | ``` 24 | 25 | and use [deepstream.io's RethinkDb storage connector](https://github.com/deepstreamIO/deepstream.io-storage-rethinkdb) with: 26 | 27 | ```js 28 | { splitChar: '/' } 29 | ``` 30 | 31 | you can now search for Harry Potter books that cost less than 15.30 like this: 32 | 33 | ```js 34 | var queryString = JSON.stringify({ 35 | table: 'book', 36 | query: [ 37 | [ 'title', 'match', '^Harry Potter.*' ], 38 | [ 'price', 'lt', 15.30 ], 39 | [ 'author.lastname', 'eq', 'Rowling' ] //nested paths work too 40 | ] 41 | }); 42 | ds.record.getList( 'search?' + queryString ); 43 | ``` 44 | 45 | and the best thing is: it's in realtime. Whenever a record that matches the search criteria is added or removed, the list will be updated accordingly. 46 | 47 | 48 | Configuration 49 | -------------------------------- 50 | Install the provider via npm and configure it to connect to both deepstream and RethinkDb 51 | 52 | ```js 53 | var SearchProvider = require( './src/provider' ); 54 | 55 | var searchProvider = new SearchProvider({ 56 | //optional, defaults to 'search' 57 | listName: 'search', 58 | 59 | /** 60 | * Only use 0 or 1 for production! 61 | 62 | * 0 = logging off 63 | * 1 = only log connection events & errors 64 | * 2 = also log subscriptions and discards 65 | * 3 = log outgoing messages 66 | */ 67 | logLevel: 3, 68 | 69 | // deepstream 70 | deepstreamUrl: 'localhost:6021', 71 | deepstreamCredentials: { username: 'rethinkdb-search-provider' }, 72 | 73 | // Instead of creating a new connection to deepstream, you can also 74 | // reuse an existing one by substituting the above with 75 | deepstreamClient: myDeepstreamClient, 76 | 77 | // rethinkdb 78 | rethinkdbConnectionParams: { 79 | host: '192.168.56.101', 80 | port: 28015, 81 | db: 'deepstream' 82 | }, 83 | 84 | //optional primary key, defaults to ds_id 85 | primaryKey: 'itemId', 86 | 87 | // Instead of creating a new connection to RethinkDb, you can also 88 | // reuse an existing one by substituting the above with 89 | rethinkDbConnection: myRethinkDbConnection 90 | }); 91 | 92 | // and start it 93 | searchProvider.start(); 94 | 95 | // it can also be stopped by calling 96 | searchProvider.stop(); 97 | ``` 98 | 99 | Searching 100 | --------------------------------- 101 | On the client you can now request dynamically populated lists of search results 102 | 103 | 104 | ```js 105 | var queryString = JSON.stringify({ 106 | table: 'book', 107 | query: [ 108 | [ 'title', 'match', '^Harry Potter.*' ], 109 | [ 'price', 'lt', 15.30 ] 110 | ] 111 | }); 112 | ds.record.getList( 'search?' + queryString ); 113 | ``` 114 | query can contain one or more conditions. Each condition is an array of [ field, operator, value ]. Supported operators are: 115 | 116 | * "eq" (equal) 117 | * "gt" (greater than) 118 | * "lt" (lesser than) 119 | * "ne" (not equal) 120 | * "match" (RegEx match) 121 | 122 | Please note that the operators are type-sensitive, so comparing `20` with `"20"` won't yield results 123 | 124 | Important! 125 | -------------------------------- 126 | Don't forget to delete your search from deepstream once you don't need it anymore, e.g. 127 | 128 | ```js 129 | bookSearchResults = ds.record.getList( 'search?' + queryString ); 130 | // use it 131 | bookSearchResults.delete(); 132 | ``` 133 | -------------------------------------------------------------------------------- /src/query-parser.js: -------------------------------------------------------------------------------- 1 | var rethinkdb = require( 'rethinkdb' ) 2 | 3 | 4 | var QueryParser = function( provider ) { 5 | this._provider = provider 6 | } 7 | 8 | /** 9 | * Parses the query string, queries are expected to 10 | * be send as JSON. The full name would look like this 11 | * 12 | * search?{ "table": "people", "query": [[ "name", "ma", "Wolf" ], [ "age", "gt", "25" ] ] } 13 | * 14 | * The structure is an array of filter conditions. Each filter condition 15 | * is expresses as [ "", "", "value" ] 16 | * 17 | * Supported operators are 18 | * 19 | * "eq" (equals) 20 | * "match" (RegEx match) 21 | * "gt" (greater than) 22 | * "ge" (greater then or equal) 23 | * "lt" (lesser than) 24 | * "le" (lesser then or equal) 25 | * "ne" (not equal) 26 | * "in" (matches a member of supplied array) 27 | * 28 | * @todo Support for OR might come in handy 29 | * @todo `in` doesn't take advantage of indexes where they exist, enhancement 30 | * 31 | * @param {Object} parsedInput the output of QueryParser.prototype.parseInput 32 | * 33 | * @public 34 | * @returns {Object} prepared rethinkdb query 35 | */ 36 | QueryParser.prototype.createQuery = function( parsedInput ) { 37 | var predicate, 38 | condition, 39 | query, 40 | i 41 | 42 | query = rethinkdb.table( parsedInput.table ) 43 | 44 | if( parsedInput.order ) { 45 | query = query.orderBy({ index: rethinkdb[ parsedInput.desc ? 'desc' : 'asc' ]( parsedInput.order ) }) 46 | } 47 | 48 | for( i = 0; i < parsedInput.query.length; i++ ) { 49 | condition = parsedInput.query[ i ] 50 | 51 | if( condition[ 1 ] !== 'in' ) { 52 | predicate = this._getRow( condition[ 0 ] )[ condition[ 1 ] ]( condition[ 2 ] ) 53 | } else { 54 | predicate = function( record ) { 55 | return rethinkdb.expr( condition[ 2 ] ).contains( record( condition[ 0 ] ) ) 56 | } 57 | } 58 | 59 | query = query.filter( predicate ) 60 | } 61 | 62 | query = query( this._provider.primaryKey ) 63 | 64 | if( parsedInput.limit ) { 65 | query = query.limit( parsedInput.limit ) 66 | } 67 | 68 | return query 69 | } 70 | 71 | /** 72 | * Receives a string like 73 | * 74 | * search?{ "table": "people", "query": [[ "name", "ma", "Wolf" ], [ "age", "gt", "25" ] ] } 75 | * 76 | * cuts off the search? part and parses the rest as JSON. Validates the resulting structure. 77 | * 78 | * @param {String} input the name of the list the user subscribed to 79 | * 80 | * @public 81 | * @returns {Object) parsedInput 82 | */ 83 | QueryParser.prototype.parseInput = function( input ) { 84 | 85 | var operators = [ 'eq', 'match', 'gt', 'ge', 'lt', 'le', 'ne', 'in' ], 86 | search, 87 | parsedInput, 88 | condition, 89 | i, 90 | index, 91 | valueIsArray 92 | 93 | index = input.indexOf( '?' ) 94 | if( index === -1 ) { 95 | return this._queryError( input, 'Missing ?' ) 96 | } 97 | 98 | search = input.substr(index + 1) 99 | 100 | try{ 101 | parsedInput = JSON.parse( search ) 102 | } catch( e ) { 103 | return this._queryError( input, 'Invalid JSON' ) 104 | } 105 | 106 | if( !parsedInput.table ) { 107 | return this._queryError( input, 'Missing parameter "table"' ) 108 | } 109 | 110 | if( !parsedInput.query ) { 111 | return this._queryError( input, 'Missing parameter "query"' ) 112 | } 113 | 114 | if( !parsedInput.order != !parsedInput.limit ) { // XOR 115 | return this._queryError( input, 'Must specify both "order" and "limit" together' ); 116 | } 117 | 118 | for( i = 0; i < parsedInput.query.length; i++ ) { 119 | condition = parsedInput.query[ i ] 120 | 121 | if( condition.length !== 3 ) { 122 | return this._queryError( input, 'Too few parameters' ) 123 | } 124 | 125 | if( operators.indexOf( condition[ 1 ] ) === -1 ) { 126 | return this._queryError( input, 'Unknown operator ' + condition[ 1 ] ) 127 | } 128 | 129 | // could use Array.isArray instead if supported 130 | valueIsArray = Object.prototype.toString.call( condition[ 2 ] ) === '[object Array]' 131 | if( condition[ 1 ] === 'in' && !valueIsArray ) { 132 | return this._queryError( input, '\'in\' operator requires a JSON array') 133 | } 134 | } 135 | 136 | return parsedInput 137 | } 138 | 139 | /** 140 | * Logs query errors 141 | * 142 | * @param {String} name 143 | * @param {String} error 144 | * 145 | * @private 146 | * @returns null 147 | */ 148 | QueryParser.prototype._queryError = function( name, error ) { 149 | this._provider.log( name, 1 ) 150 | this._provider.log( 'QUERY ERROR | ' + error, 1 ) 151 | return null 152 | } 153 | 154 | /** 155 | * Creates a ReQL row statement out of a simple or nested path 156 | * 157 | * @param {String} path the path to search for 158 | * 159 | * @private 160 | * @returns {[ReQL.Row]} ReQL row object 161 | */ 162 | QueryParser.prototype._getRow = function( path ) { 163 | var parts = path.split( /[\[\]\.]/g ).filter( val => val.trim().length > 0 ) 164 | var row = rethinkdb.row( parts[ 0 ] ); 165 | 166 | for( var i = 1; i < parts.length; i++ ) { 167 | row = row( parts[ i ] ); 168 | } 169 | 170 | return row; 171 | }; 172 | 173 | module.exports = QueryParser 174 | -------------------------------------------------------------------------------- /test/happy-pathSpec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect 2 | 3 | const testHelper = require( './tools/test-helper' ) 4 | const connectionParams = require( './tools/connection-params' ) 5 | const Deepstream = require( 'deepstream.io' ) 6 | const RethinkDBStorageConnector = require( 'deepstream.io-storage-rethinkdb' ) 7 | var server = null 8 | 9 | describe( 'the provider', () => { 10 | var provider 11 | var ds 12 | var spanishBooks 13 | 14 | var spanishBooksQuery = JSON.stringify({ 15 | table: connectionParams.testTable, 16 | query: [[ 'language', 'eq', 'Spanish' ]] 17 | }) 18 | 19 | before((done) => { 20 | testHelper.createTestTable( done ) 21 | }) 22 | 23 | before((done) => { 24 | server = new Deepstream() 25 | server.set( 'storage', new RethinkDBStorageConnector( { 26 | host: connectionParams.rethinkdb.host, 27 | port: connectionParams.rethinkdb.port, 28 | database: connectionParams.rethinkdb.db, 29 | defaultTable: connectionParams.testTable 30 | })) 31 | server.set( 'storageExclusion', /^search.*/ ) 32 | server.set( 'port', 7071) 33 | server.set( 'showLogo', false) 34 | server.set( 'logger', { 35 | setLogLevel: function() {}, 36 | log: function() {}, 37 | isReady: true 38 | } ) 39 | server.on('started', () => { 40 | done() 41 | }) 42 | server.start() 43 | }) 44 | 45 | after( (done) => { 46 | server.on('stopped', done ) 47 | server.stop() 48 | }) 49 | 50 | it( 'starts', ( done ) => { 51 | testHelper.startProvider(( _provider ) => { 52 | provider = _provider 53 | done() 54 | }) 55 | }) 56 | 57 | it( 'establishes a connection to deepstream', ( done ) => { 58 | testHelper.connectToDeepstream(( err, _ds ) => { 59 | ds = _ds 60 | done( err ) 61 | }) 62 | }) 63 | 64 | it( 'can retrieve records from the table', ( done ) => { 65 | var record = ds.record.getRecord( 'lor' ) 66 | record.whenReady(() => { 67 | expect( record.get( 'title' ) ).to.equal( 'The Lord of the Rings' ) 68 | done() 69 | }) 70 | }) 71 | 72 | it( 'issues a simple search for books in Spanish and finds Don Quixote', ( done ) => { 73 | var subscription = (arg) => { 74 | expect( arg ).to.deep.equal([ 'don' ]) 75 | spanishBooks.unsubscribe( subscription ) 76 | spanishBooks.discard() 77 | done() 78 | } 79 | spanishBooks = ds.record.getList( 'search?' + spanishBooksQuery ) 80 | spanishBooks.subscribe( subscription ) 81 | }) 82 | 83 | it( 'inserts a new Spanish book and the search gets notified', ( done ) => { 84 | ds.record.getRecord( 'ohy' ).set({ 85 | title: 'Cien años de soledad', 86 | author: 'Gabriel García Márquez', 87 | language: 'Spanish', 88 | released: 1967, 89 | copiesSold: 50000000 90 | }) 91 | 92 | var update = false; 93 | var subscription = (arg) => { 94 | if( !update ) { 95 | expect( arg ).to.deep.equal([ 'don' ]) 96 | update = true 97 | return 98 | } 99 | 100 | expect( arg ).to.deep.equal([ 'don', 'ohy' ]) 101 | spanishBooks.unsubscribe( subscription ) 102 | spanishBooks.discard() 103 | done() 104 | } 105 | spanishBooks = ds.record.getList( 'search?' + spanishBooksQuery ) 106 | spanishBooks.subscribe( subscription ) 107 | }) 108 | 109 | it( 'issues a search for all books published between 1700 and 1950', ( done ) => { 110 | var query = JSON.stringify({ 111 | table: connectionParams.testTable, 112 | query: [ 113 | [ 'released', 'gt', 1700 ], 114 | [ 'released', 'lt', 1950 ] 115 | ] 116 | }) 117 | var subscription = ( entries ) => { 118 | expect( entries ).to.deep.equal([ 'tct', 'tlp' ]) 119 | olderBooks.unsubscribe( subscription ) 120 | olderBooks.discard() 121 | done() 122 | } 123 | var olderBooks = ds.record.getList( 'search?' + query ) 124 | olderBooks.subscribe( subscription ) 125 | }) 126 | 127 | // TODO: it correctly handles an order field without an index 128 | 129 | it( 'issues a search for the most recent book', ( done ) => { 130 | var query = JSON.stringify({ 131 | table: connectionParams.testTable, 132 | query: [], 133 | order: 'released', 134 | desc: true, 135 | limit: 1 136 | }) 137 | 138 | var update = false; 139 | var subscription = ( arg ) => { 140 | if( !update ) { 141 | expect( arg ).to.deep.equal([ 'hrp' ]) 142 | update = true 143 | 144 | ds.record.getRecord( 'twl' ).set({ 145 | title: 'Twilight', 146 | author: 'Stephanie Meyer', 147 | language: 'English', 148 | released: 2005, 149 | copiesSold: 47000000 150 | }) 151 | 152 | return 153 | } 154 | 155 | expect( arg ).to.deep.equal([ 'twl' ]) 156 | recentBook.unsubscribe( subscription ) 157 | recentBook.discard() 158 | done() 159 | } 160 | var recentBook = ds.record.getList( 'search?' + query ) 161 | recentBook.subscribe( subscription ) 162 | }); 163 | 164 | it( 'cleans up', ( done ) => { 165 | ds.record.getRecord( 'ohy' ).delete() 166 | ds.record.getRecord( 'twl' ).delete() 167 | testHelper.cleanUp( provider, ds, done ) 168 | }) 169 | 170 | }) 171 | -------------------------------------------------------------------------------- /src/search.js: -------------------------------------------------------------------------------- 1 | const r = require( 'rethinkdb' ) 2 | 3 | /** 4 | * This class represents a single realtime search query against RethinkDb. 5 | * 6 | * It creates two cursors, one to 7 | * retrieve the initial matches, one to listen for incoming changes. It then 8 | * creates a deepstream list and populates it with the changes 9 | * 10 | * Important: Deepstream notifies this provider once all subscriptions for list have been 11 | * removed - which presents a conundrum since the provider is a subscriber in its own right, 12 | * so will never be notified. Instead it listens to the list being deleted - which isn't an ideal 13 | * solution since it either requires namespacing the lists with a username or deleting the lists 14 | * for all subscribers. 15 | * 16 | * @constructor 17 | * 18 | * @param {Provider} provider 19 | * @param {Object} query The query as returned by _createQuery 20 | * @param {String} listName The full name of the list that the client subscribed to 21 | * @param {Connection} rethinkdbConnection 22 | * @param {DeepstreamClient} deepstreamClient 23 | */ 24 | var Search = function( provider, query, listName, rethinkdbConnection, deepstreamClient ) { 25 | this.subscriptions = 0 26 | 27 | this._provider = provider 28 | this._list = deepstreamClient.record.getList( listName ) 29 | this._changeFeedCursor = null 30 | this._initialValues = Object.create( null ) // new Set() would be better 31 | 32 | query 33 | .changes({ includeStates: true, includeInitial: true, squash: false }) 34 | .run( rethinkdbConnection, this._onChange.bind( this ) ) 35 | } 36 | 37 | /** 38 | * Closes the RethinkDb change feed cursor. It also deletes the list if called 39 | * as a result of an unsubscribe call to the record listener, but not if called 40 | * as a result of the list being deleted. 41 | * 42 | * @public 43 | * @returns {void} 44 | */ 45 | Search.prototype.destroy = function() { 46 | 47 | if( this._list ) { 48 | this._provider.log( 'Removing search ' + this._list.name, 2 ) 49 | this._list.delete() 50 | this._list = null 51 | } 52 | 53 | if( this._changeFeedCursor ) { 54 | this._changeFeedCursor.close() 55 | this._changeFeedCursor = null 56 | } 57 | } 58 | 59 | /** 60 | * Processes an incoming change notification. This might be a new 61 | * document matching the search criteria or an existing one not matching them 62 | * any longer 63 | * 64 | * @param {RqlRuntimeError} error or null 65 | * @param {change feed cursor} cursor 66 | * 67 | * @private 68 | * @returns {void} 69 | */ 70 | Search.prototype._onChange = function( error, cursor ) { 71 | if( error ) { 72 | if( this._initialValues ) { 73 | this._onError( 'Error while retrieving initial value: ' + error.toString() ) 74 | } else { 75 | this._onError( 'Error while receiving change notification: ' + error ) 76 | } 77 | } else { 78 | this._changeFeedCursor = cursor; 79 | cursor.each( this._readChange.bind( this ) ) 80 | } 81 | } 82 | 83 | /** 84 | * Reads the incoming change document and distuinguishes 85 | * between "status documents" and actual changes 86 | * 87 | * @param {RqlRuntimeError} cursorError or null 88 | * @param {Object} change A map with an old_val and a new_val key 89 | * 90 | * @returns {void} 91 | */ 92 | Search.prototype._readChange = function( cursorError, change ) { 93 | if( cursorError ) { 94 | this._onError( 'cursor error on change: ' + cursorError.toString() ) 95 | } 96 | else if( change.state ) { 97 | if( change.state === 'ready' ) { 98 | this._populateList() 99 | } 100 | } 101 | else { 102 | if( this._initialValues ) { 103 | this._processInitialValues( change ) 104 | } else { 105 | this._processChange( change ) 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * Retrieves the primary keys from the list of retrieved documents 112 | * and populates the list with them 113 | * 114 | * @param {Array} values the full retrieved documents 115 | * 116 | * @private 117 | * @returns {void} 118 | */ 119 | Search.prototype._populateList = function() { 120 | var recordNames = [], 121 | k 122 | 123 | for( k in this._initialValues ) { 124 | if( Object.prototype.hasOwnProperty.call( this._initialValues, k ) ) { 125 | recordNames.push( k ) 126 | } 127 | } 128 | delete this._initialValues; 129 | 130 | this._provider.log( 'Found ' + recordNames.length + ' initial matches for ' + this._list.name, 3 ) 131 | this._list.setEntries( recordNames ) 132 | } 133 | 134 | /** 135 | * Adds/removes initial values till rethink declares the values ready 136 | * 137 | * @param {Object} change A map with an old_val and a new_val key 138 | * 139 | * @private 140 | * @returns {void} 141 | */ 142 | Search.prototype._processInitialValues = function( change ) { 143 | if( change.new_val ) { 144 | this._provider.log( 'Added 1 initial entry to ' + this._list.name, 3 ) 145 | this._initialValues[ change.new_val ] = true 146 | } 147 | 148 | if( change.old_val ) { 149 | this._provider.log( 'Removed 1 initial entry from ' + this._list.name, 3 ) 150 | delete this._initialValues[ change.old_val ] 151 | } 152 | } 153 | 154 | /** 155 | * Replaces an entry in the list with a single action 156 | * 157 | * @param change an object with old_val and new_val set 158 | * 159 | * @private 160 | * @returns {void} 161 | */ 162 | Search.prototype._replaceEntry = function( change ) { 163 | var currentEntires = this._list.getEntries(), 164 | entries, 165 | 166 | entries = currentEntires.filter( value => value !== change.old_val ) 167 | entries.push( change.new_val ) 168 | 169 | this._list.setEntries( entries ) 170 | } 171 | 172 | /** 173 | * Differentiates between additions and deletions 174 | * 175 | * @param {Object} change A map with an old_val and a new_val key 176 | * 177 | * @private 178 | * @returns {void} 179 | */ 180 | Search.prototype._processChange = function( change ) { 181 | if( change.old_val !== null && change.new_val !== null ) { 182 | this._provider.log( 'Replacing 1 entry to ' + this._list.name, 3 ) 183 | this._replaceEntry( change ); 184 | } 185 | 186 | if( change.old_val === null ) { 187 | this._provider.log( 'Added 1 new entry to ' + this._list.name, 3 ) 188 | this._list.addEntry( change.new_val ) 189 | } 190 | 191 | if( change.new_val === null ) { 192 | this._provider.log( 'Removed 1 entry from ' + this._list.name, 3 ) 193 | this._list.removeEntry( change.old_val ) 194 | } 195 | } 196 | 197 | /** 198 | * Error callback 199 | * 200 | * @param {String} error 201 | * 202 | * @private 203 | * @returns {void} 204 | */ 205 | Search.prototype._onError = function( error ) { 206 | this._provider.log( 'Error for ' + this._list.name + ': ' + error, 1 ) 207 | } 208 | 209 | 210 | module.exports = Search 211 | -------------------------------------------------------------------------------- /src/provider.js: -------------------------------------------------------------------------------- 1 | var rethinkdb = require( 'rethinkdb' ), 2 | Search = require( './search' ), 3 | DeepstreamClient = require( 'deepstream.io-client-js' ), 4 | EventEmitter = require( 'events' ).EventEmitter, 5 | QueryParser = require( './query-parser' ), 6 | util = require( 'util' ); 7 | 8 | /** 9 | * A data provider that provides dynamically 10 | * created lists based on search parameters 11 | * using rethinkdb 12 | * 13 | * @constructor 14 | * @extends EventEmitter 15 | * 16 | * @param {Object} config please consuld README.md for details 17 | */ 18 | var Provider = function( config ) { 19 | this.isReady = false; 20 | this.primaryKey = config.primaryKey || 'ds_id'; 21 | this._config = config; 22 | this._queryParser = new QueryParser( this ); 23 | this._logLevel = config.logLevel !== undefined ? config.logLevel : 1; 24 | this._rethinkDbConnection = null; 25 | this._deepstreamClient = null; 26 | this._listName = config.listName || 'search'; 27 | this._searches = {}; 28 | }; 29 | 30 | util.inherits( Provider, EventEmitter ); 31 | 32 | /** 33 | * Starts the provider. The provider will emit a 34 | * 'ready' event once started 35 | * 36 | * @public 37 | * @returns void 38 | */ 39 | Provider.prototype.start = function() { 40 | this._initialiseDbConnection(); 41 | }; 42 | 43 | /** 44 | * Stops the provider. Closes the deepstream 45 | * connection and disconnects from RethinkDb 46 | * 47 | * @public 48 | * @returns void 49 | */ 50 | Provider.prototype.stop = function() { 51 | this._deepstreamClient.close(); 52 | this._rethinkDbConnection.close(); 53 | }; 54 | 55 | /** 56 | * Logs messages to StdOut 57 | * 58 | * @todo introduce log level and (potentially) add logging 59 | * for every transaction 60 | * 61 | * @param {String} message 62 | * 63 | * @public 64 | * @returns {void} 65 | */ 66 | Provider.prototype.log = function( message, level ) { 67 | if( this._logLevel < level ) { 68 | return; 69 | } 70 | 71 | var date = new Date(), 72 | time = date.toLocaleTimeString() + ':' + date.getMilliseconds(); 73 | 74 | console.log( time + ' | ' + message ); 75 | }; 76 | 77 | 78 | /** 79 | * Creates the connection to RethinkDB. If the connection 80 | * is unsuccessful, an error will be thrown. If the configuration 81 | * contains an active connection to RethinkDb instead of connection 82 | * parameters, it will be used instead and the provider will move 83 | * on to connect to deepstream immediatly 84 | * 85 | * @private 86 | * @returns void 87 | */ 88 | Provider.prototype._initialiseDbConnection = function() { 89 | this.log( 'Initialising RethinkDb Connection', 1 ); 90 | 91 | if( this._config.rethinkDbConnection ) { 92 | this._rethinkDbConnection = this._config.rethinkDbConnection; 93 | this._initialiseDeepstreamClient(); 94 | } else { 95 | if( !this._config.rethinkdbConnectionParams ) { 96 | throw new Error( 'Can\'t connect to rethinkdb, neither connection nor connection parameters provided' ); 97 | } 98 | 99 | rethinkdb.connect( this._config.rethinkdbConnectionParams, this._onRethinkdbConnection.bind( this ) ); 100 | } 101 | }; 102 | 103 | /** 104 | * Callback for established RethinkDb connections. Initialises the deepstream connection 105 | * 106 | * @param {RqlDriverError} error (or null for no error) 107 | * @param {RethinkdbConnection} connection 108 | * 109 | * @private 110 | * @returns void 111 | */ 112 | Provider.prototype._onRethinkdbConnection = function( error, connection ) { 113 | if( error ) { 114 | throw new Error( 'Error while connecting to RethinkDb: ' + error.toString(), 1 ); 115 | } else { 116 | this.log( 'RethinkDb connection established', 1 ); 117 | this._rethinkDbConnection = connection; 118 | this._initialiseDeepstreamClient(); 119 | } 120 | }; 121 | 122 | /** 123 | * Connect to deepstream via TCP. If an active deepstream connection 124 | * is provided instead of connection parameters, it will be used and the provider 125 | * moves on to the final step of the initialisation sequence immediatly 126 | * 127 | * @private 128 | * @returns void 129 | */ 130 | Provider.prototype._initialiseDeepstreamClient = function() { 131 | this.log( 'Initialising Deepstream connection', 1 ); 132 | 133 | if( this._config.deepstreamClient ) { 134 | this._deepstreamClient = this._config.deepstreamClient; 135 | this.log( 'Deepstream connection established', 1 ); 136 | this._ready(); 137 | } else { 138 | if( !this._config.deepstreamUrl ) { 139 | throw new Error( 'Can\'t connect to deepstream, neither deepstreamClient nor deepstreamUrl were provided', 1 ); 140 | } 141 | 142 | if( !this._config.deepstreamCredentials ) { 143 | throw new Error( 'Missing configuration parameter deepstreamCredentials', 1 ); 144 | } 145 | 146 | this._deepstreamClient = new DeepstreamClient( this._config.deepstreamUrl ); 147 | this._deepstreamClient.on( 'error', function( error ) { 148 | console.log( error ) 149 | } ); 150 | this._deepstreamClient.login( this._config.deepstreamCredentials, this._onDeepstreamLogin.bind( this ) ); 151 | } 152 | }; 153 | 154 | /** 155 | * Callback for logins. If the login was successful the provider moves on 156 | * to the final step of the initialisation sequence. 157 | * 158 | * @param {Boolean} success 159 | * @param {String} error A deepstream error constant 160 | * @param {String} message The message returned by the permissionHandler 161 | * 162 | * @private 163 | * @returns void 164 | */ 165 | Provider.prototype._onDeepstreamLogin = function( success, error, message ) { 166 | if( success ) { 167 | this.log( 'Connection to deepstream established', 1 ); 168 | this._ready(); 169 | } else { 170 | this.log( 'Can\'t connect to deepstream: ' + message, 1 ); 171 | } 172 | }; 173 | 174 | /** 175 | * Last step in the initialisation sequence. Listens for the specified pattern and emits 176 | * the ready event 177 | * 178 | * @private 179 | * @returns void 180 | */ 181 | Provider.prototype._ready = function() { 182 | var pattern = this._listName + '[\\?].*'; 183 | this.log( 'listening for ' + pattern, 1 ); 184 | this._deepstreamClient.record.listen( pattern, this._onSubscription.bind( this ) ); 185 | this.log( 'rethinkdb search provider ready', 1 ); 186 | this.isReady = true; 187 | this.emit( 'ready' ); 188 | }; 189 | 190 | /** 191 | * Callback for the 'listen' method. Gets called everytime a new 192 | * subscription to the specified pattern is made. Parses the 193 | * name and - if its the first subscription made to this pattern - 194 | * creates a new instance of Search 195 | * 196 | * 197 | * @param {String} name The listname the client subscribed to 198 | * @param {Boolean} subscribed Whether a subscription had been made or removed. 199 | * 200 | * @private 201 | * @returns {void} 202 | */ 203 | Provider.prototype._onSubscription = function( name, subscribed, response ) { 204 | 205 | if( subscribed ) { 206 | response.accept(); 207 | this.log( 'received subscription for ' + name, 2 ); 208 | this._onSubscriptionAdded( name ); 209 | } else { 210 | this.log( 'discard subscription for ' + name, 2 ); 211 | this._onSubscriptionRemoved( name ); 212 | } 213 | 214 | 215 | }; 216 | 217 | /** 218 | * When a search has been started 219 | * 220 | * @private 221 | * @returns {void} 222 | */ 223 | Provider.prototype._onSubscriptionAdded = function( name ) { 224 | var parsedInput = this._queryParser.parseInput( name ), 225 | query; 226 | 227 | if( parsedInput === null ) { 228 | return; 229 | } 230 | 231 | query = this._queryParser.createQuery( parsedInput ); 232 | this._searches[ name ] = new Search( this, query, name, this._rethinkDbConnection, this._deepstreamClient ); 233 | }; 234 | 235 | /** 236 | * When a search has been removed 237 | * 238 | * @private 239 | * @returns {void} 240 | */ 241 | Provider.prototype._onSubscriptionRemoved = function( name ) { 242 | this._searches[ name ].destroy(); 243 | delete this._searches[ name ]; 244 | }; 245 | 246 | module.exports = Provider; 247 | --------------------------------------------------------------------------------