├── .gitignore ├── README.md ├── createDb.js ├── nebula.js ├── package.json ├── rethinkdb_data ├── b9e4d082-67fd-48d8-91bf-69a3f8b056fe ├── log_file └── metadata ├── src ├── createDatabase.js ├── lib │ ├── helpers.js │ ├── lib.js │ └── set.js ├── nebuladb.js ├── read.js ├── server.js └── write.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | node_modules/* 3 | .DS_Store 4 | rethinkdb_data/ 5 | rethinkdb_data/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NebulaDB 2 | ====== 3 | NebulaDB implements a simple and intuitive graph database on top of [RethinkDB](https://www.rethinkdb.com/). It is experimental and is currently not as robust as it could be, but I encourage folks to try it out and make pull requests or open issues. Because RethinkDB has a solid platform problems related to persistence can be ignored and a clean expressive interface can more easily be attained. 4 | 5 | To run the database first install [RethinkDB](https://www.rethinkdb.com/) and get your RethinkDB server up and running. Next clone this repository and run the following commands: 6 | 7 | To create a new Nebula database in Rethink: 8 | ```shell 9 | node createDB mydatabase 10 | ``` 11 | You will see a success message if everything goes well. 12 | 13 | 14 | Next, run the server to listen for reads and writes on your database. This command takes the name of your database as a single option: 15 | ```shell 16 | node nebula mydatabase 17 | ``` 18 | You will see a message telling you that the server is listening for requests. Next, I reccommend using [Postman](https://www.getpostman.com/) to experiment with the API. You simply send Nebula queries in raw text to the API like this: 19 | 20 | (POST) /save "['I like toast']" 21 | (POST) /query "['I like *']" => response: ['toast'] 22 | 23 | It is also possible to use the NebulaDB module by itself instead of accessing it through the server. Look at the test.js file in the root directory for an example of how to do this. 24 | 25 | Documentation 26 | ------------- 27 | 28 | NebulaDB uses a simple graph based schema that looks like this: 29 | ```javascript 30 | [ source, relation, target ] 31 | ``` 32 | Entering three strings in this way creates three nodes in the database and sets up a special (Node)-[Link]-(Node) relationship between them. You can also use the reserved symbol '->' in the relation position to indicate that the source node has the state indicated by the target node, for example: 33 | ```javascript 34 | 'john -> admin' 35 | 'john -> user' 36 | 'john first_name John' 37 | ``` 38 | This symbol '->' denotes a simple state. It is used for boolean properties, in other words properties that a node either has or does not have. 39 | ```javascript 40 | db.save("John -> admin"); 41 | db.save("Mary -> user"); 42 | db.query("Mary -> admin"); // returns false 43 | ``` 44 | There are two ways to query the database. The first way is to simply query a pattern. The database will response with a boolean that tell you whether or not the pattern exists in the database. 45 | ```javascript 46 | 'john -> admin' //=> true 47 | 'john -> founder' //=> false 48 | ``` 49 | The second way to query is by using an asterisk to indicate which kinds of data you want to see. Here are some examples preceded by the save queries that would result in the results shown: 50 | ```javascript 51 | db.save('john -> user') 52 | db.save('john -> founder') 53 | db.save('john first_name John') 54 | db.save('john nick_name Johnny') 55 | 56 | db.query('john -> *') 57 | // returns array of simple states: ['admin', 'founder'] 58 | db.query('john first_name *') 59 | // returns the target pointed to by the node in the middle position: ['John'] 60 | db.query('john * *') 61 | // returns hash of all target states: {simple: ['admin', 'founder' ], custom: { first_name: ['John'], nick_name: ['Johnny'] } } 62 | db.query('* -> founder') 63 | // returns array of all states that obtain the admin state: ['john'] 64 | db.query('* * John') 65 | // returns hash of all source states: { simple: [], custom: { first_name: 'John' } } 66 | ``` 67 | 68 | Querying 69 | -------- 70 | ```javascript 71 | db.query('a b c', callback) 72 | ``` 73 | The query method tests the database using the given query and passes the result into the callack. There are currently eight types of queries: 74 | ```javascript 75 | [a, b, c] // does item a have relation b to item c -> boolean 76 | [a, ->, c] // does item a have state c -> boolean 77 | [a, ->, *] // what are the states of item a -> array 78 | [a, b, *] // what is the item with relation b to item a -> object 79 | [a, *, *] // what are all the relation/target pairs for item a -> object 80 | [*, *, c] // what are all the source/relation pairs for item c 81 | [*, ->, c] // what are all the states that obtain state c 82 | [*, b, c] // what item(s) have relation b to c 83 | ``` 84 | Editing 85 | ------- 86 | ```javascript 87 | db.removeLink('a b c'); 88 | ``` 89 | The removeLink method removes a relationship between A and node B. This feature is not currently implemented, but will be soon. 90 | -------------------------------------------------------------------------------- /createDb.js: -------------------------------------------------------------------------------- 1 | var program = require('commander'); 2 | var createDb = require('./src/createDatabase.js'); 3 | 4 | program.parse(process.argv); 5 | 6 | var name = program.args[0]; 7 | 8 | createDb(name); 9 | -------------------------------------------------------------------------------- /nebula.js: -------------------------------------------------------------------------------- 1 | var program = require('commander'); 2 | 3 | program.parse(process.argv); 4 | 5 | var name = program.args[0]; 6 | 7 | var nebulaServer = require('./src/server.js')(name); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nebuladb", 3 | "version": "1.1.0", 4 | "description": "NebulaDB Graph Database", 5 | "main": "nebula.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "James Edwards", 13 | "license": "MIT", 14 | "dependencies": { 15 | "commander": "^2.7.1", 16 | "rethinkdb": "^2.2.0" 17 | }, 18 | "devDependencies": {}, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/incrediblesound/nebuladb.git" 22 | }, 23 | "keywords": [ 24 | "graph", 25 | "database" 26 | ], 27 | "bugs": { 28 | "url": "https://github.com/incrediblesound/nebuladb/issues" 29 | }, 30 | "homepage": "https://github.com/incrediblesound/nebuladb" 31 | } 32 | -------------------------------------------------------------------------------- /rethinkdb_data/b9e4d082-67fd-48d8-91bf-69a3f8b056fe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incrediblesound/nebulaDB/496dae2be91da2a061725383480dcf36216d66c0/rethinkdb_data/b9e4d082-67fd-48d8-91bf-69a3f8b056fe -------------------------------------------------------------------------------- /rethinkdb_data/log_file: -------------------------------------------------------------------------------- 1 | 2015-12-11T08:29:05.104798000 0.630182s info: Initializing directory /Users/james/projects4/nebulaDB/rethinkdb_data 2 | 2015-12-11T08:29:05.106472000 0.631856s info: Creating a default database for your convenience. (This is because you ran 'rethinkdb' without 'create', 'serve', or '--join', and the directory '/Users/james/projects4/nebulaDB/rethinkdb_data' did not already exist or is empty.) 3 | 2015-12-11T08:29:05.106521000 0.631905s info: Running rethinkdb 1.15.2 (CLANG 4.2 (clang-425.0.28))... 4 | 2015-12-11T08:29:05.115309000 0.640693s info: Running on Darwin 14.0.0 x86_64 5 | 2015-12-11T08:29:05.115351000 0.640735s info: Using cache size of 100 MB 6 | 2015-12-11T08:29:05.115379000 0.640762s warn: Cache size does not leave much memory for server and query overhead (available memory: 1043 MB). 7 | 2015-12-11T08:29:05.115396000 0.640780s warn: Cache size is very low and may impact performance. 8 | 2015-12-11T08:29:05.115411000 0.640794s info: Loading data from directory /Users/james/projects4/nebulaDB/rethinkdb_data 9 | 2015-12-11T08:29:05.237055000 0.762439s info: Listening for intracluster connections on port 29015 10 | 2015-12-11T08:29:05.238361000 0.763745s info: Listening for client driver connections on port 28015 11 | 2015-12-11T08:29:05.238414000 0.763798s info: Listening for administrative HTTP connections on port 8080 12 | 2015-12-11T08:29:05.238423000 0.763807s info: Listening on addresses: 127.0.0.1, ::1 13 | 2015-12-11T08:29:05.238430000 0.763813s info: To fully expose RethinkDB on the network, bind to all addresses 14 | 2015-12-11T08:29:05.238437000 0.763820s info: by running rethinkdb with the `--bind all` command line option. 15 | 2015-12-11T08:29:05.238443000 0.763827s info: Server ready 16 | 2015-12-11T08:29:30.665768000 26.191834s info: Server got SIGINT from pid 836, uid 501; shutting down... 17 | 2015-12-11T08:29:30.666043000 26.192110s info: Shutting down client connections... 18 | 2015-12-11T08:29:30.666443000 26.192509s info: All client connections closed. 19 | 2015-12-11T08:29:30.666447000 26.192514s info: Shutting down storage engine... (This may take a while if you had a lot of unflushed data in the writeback cache.) 20 | 2015-12-11T08:29:30.666479000 26.192545s info: Storage engine shut down. 21 | 2015-12-11T10:22:51.971974000 0.617753s notice: Running rethinkdb 2.2.1 (CLANG 7.0.0 (clang-700.1.76))... 22 | 2015-12-11T10:22:51.977441000 0.623215s notice: Running on Darwin 14.0.0 x86_64 23 | 2015-12-11T10:22:51.977480000 0.623252s notice: Loading data from directory /Users/james/projects4/nebulaDB/rethinkdb_data 24 | 2015-12-11T10:22:51.981249000 0.627022s notice: Migrating cluster metadata to v2.2 25 | 2015-12-11T10:22:51.982339000 0.628111s notice: Migrating file to serializer version 2.2. 26 | 2015-12-11T10:22:51.997955000 0.643728s notice: Migrating auth metadata 27 | 2015-12-11T10:22:52.017557000 0.663329s info: Automatically using cache size of 171 MB 28 | 2015-12-11T10:22:52.018642000 0.664414s notice: Listening for intracluster connections on port 29015 29 | 2015-12-11T10:22:52.021898000 0.667671s notice: Listening for client driver connections on port 28015 30 | 2015-12-11T10:22:52.022043000 0.667816s notice: Listening for administrative HTTP connections on port 8080 31 | 2015-12-11T10:22:52.022047000 0.667820s notice: Listening on addresses: 127.0.0.1, ::1 32 | 2015-12-11T10:22:52.022048000 0.667821s notice: To fully expose RethinkDB on the network, bind to all addresses by running rethinkdb with the `--bind all` command line option. 33 | 2015-12-11T10:22:52.022051000 0.667823s notice: Server ready, "Jamess_MacBook_Pro_4_local_6i2" c031fc8e-907c-42fa-a953-a41eaf3a12da 34 | 2015-12-11T10:23:04.354202000 13.000372s notice: Server got SIGINT from pid 836, uid 501; shutting down... 35 | 2015-12-11T10:23:04.354475000 13.000646s notice: Shutting down client connections... 36 | 2015-12-11T10:23:04.354577000 13.000748s notice: All client connections closed. 37 | 2015-12-11T10:23:04.354582000 13.000752s notice: Shutting down storage engine... (This may take a while if you had a lot of unflushed data in the writeback cache.) 38 | 2015-12-11T10:23:04.357475000 13.003646s notice: Storage engine shut down. 39 | 2015-12-11T10:23:15.051027000 0.004998s notice: Running rethinkdb 2.2.1 (CLANG 7.0.0 (clang-700.1.76))... 40 | 2015-12-11T10:23:15.053964000 0.007933s notice: Running on Darwin 14.0.0 x86_64 41 | 2015-12-11T10:23:15.054014000 0.007983s notice: Loading data from directory /Users/james/projects4/nebulaDB/rethinkdb_data 42 | 2015-12-11T10:23:15.072404000 0.026373s info: Automatically using cache size of 170 MB 43 | 2015-12-11T10:23:15.072922000 0.026891s notice: Listening for intracluster connections on port 29015 44 | 2015-12-11T10:23:15.074189000 0.028157s notice: Listening for client driver connections on port 28015 45 | 2015-12-11T10:23:15.074308000 0.028277s notice: Listening for administrative HTTP connections on port 8080 46 | 2015-12-11T10:23:15.074312000 0.028280s notice: Listening on addresses: 127.0.0.1, ::1 47 | 2015-12-11T10:23:15.074313000 0.028281s notice: To fully expose RethinkDB on the network, bind to all addresses by running rethinkdb with the `--bind all` command line option. 48 | 2015-12-11T10:23:15.074315000 0.028283s notice: Server ready, "Jamess_MacBook_Pro_4_local_6i2" c031fc8e-907c-42fa-a953-a41eaf3a12da 49 | 2015-12-11T10:24:40.342620000 85.299035s info: Table a267e2f6-05f5-48fb-801b-dd6d090c989d: Added replica on this server. 50 | 2015-12-11T10:24:42.117370000 87.073842s info: Table a267e2f6-05f5-48fb-801b-dd6d090c989d: Starting a new Raft election for term 1. 51 | 2015-12-11T10:24:42.128859000 87.085332s info: Table a267e2f6-05f5-48fb-801b-dd6d090c989d: This server is Raft leader for term 1. Latest log index is 0. 52 | 2015-12-11T20:09:18.002439000 21195.991346s info: Table a267e2f6-05f5-48fb-801b-dd6d090c989d: Configuration is changing. 53 | 2015-12-11T21:30:19.745134000 26057.872717s info: Table a267e2f6-05f5-48fb-801b-dd6d090c989d: Configuration is changing. 54 | 2015-12-12T18:36:00.808332000 47755.852142s notice: Removing file /Users/james/projects4/nebulaDB/rethinkdb_data/a267e2f6-05f5-48fb-801b-dd6d090c989d 55 | 2015-12-12T18:36:00.808349000 47755.852154s info: Table a267e2f6-05f5-48fb-801b-dd6d090c989d: Deleted the table. 56 | 2015-12-12T18:41:24.750295000 48079.803433s info: Table 348efdc3-53f8-4f18-872e-4527c328eb35: Added replica on this server. 57 | 2015-12-12T18:41:26.025763000 48081.078901s info: Table 348efdc3-53f8-4f18-872e-4527c328eb35: Starting a new Raft election for term 1. 58 | 2015-12-12T18:41:26.035623000 48081.088760s info: Table 348efdc3-53f8-4f18-872e-4527c328eb35: This server is Raft leader for term 1. Latest log index is 0. 59 | 2015-12-12T18:41:43.545898000 48098.599548s notice: Removing file /Users/james/projects4/nebulaDB/rethinkdb_data/348efdc3-53f8-4f18-872e-4527c328eb35 60 | 2015-12-12T18:41:43.545905000 48098.599555s info: Table 348efdc3-53f8-4f18-872e-4527c328eb35: Deleted the table. 61 | 2015-12-12T18:41:48.581449000 48103.635270s info: Table 5560c992-edaa-435d-88fe-76208808e19e: Added replica on this server. 62 | 2015-12-12T18:41:50.145914000 48105.199734s info: Table 5560c992-edaa-435d-88fe-76208808e19e: Starting a new Raft election for term 1. 63 | 2015-12-12T18:41:50.157025000 48105.210846s info: Table 5560c992-edaa-435d-88fe-76208808e19e: This server is Raft leader for term 1. Latest log index is 0. 64 | 2015-12-12T18:42:55.151178000 48170.206877s notice: Removing file /Users/james/projects4/nebulaDB/rethinkdb_data/5560c992-edaa-435d-88fe-76208808e19e 65 | 2015-12-12T18:42:55.151186000 48170.206884s info: Table 5560c992-edaa-435d-88fe-76208808e19e: Deleted the table. 66 | 2015-12-12T18:43:02.737463000 48177.793389s info: Table b5fa847a-fb86-41f1-aa52-de882cfb10c7: Added replica on this server. 67 | 2015-12-12T18:43:03.939533000 48178.995459s info: Table b5fa847a-fb86-41f1-aa52-de882cfb10c7: Starting a new Raft election for term 1. 68 | 2015-12-12T18:43:03.952482000 48179.008408s info: Table b5fa847a-fb86-41f1-aa52-de882cfb10c7: This server is Raft leader for term 1. Latest log index is 0. 69 | 2015-12-12T18:43:26.606457000 48201.663066s notice: Removing file /Users/james/projects4/nebulaDB/rethinkdb_data/b5fa847a-fb86-41f1-aa52-de882cfb10c7 70 | 2015-12-12T18:43:26.606464000 48201.663072s info: Table b5fa847a-fb86-41f1-aa52-de882cfb10c7: Deleted the table. 71 | 2015-12-12T18:43:31.019758000 48206.076481s info: Table b9e4d082-67fd-48d8-91bf-69a3f8b056fe: Added replica on this server. 72 | 2015-12-12T18:43:32.140853000 48207.197633s info: Table b9e4d082-67fd-48d8-91bf-69a3f8b056fe: Starting a new Raft election for term 1. 73 | 2015-12-12T18:43:32.152094000 48207.208874s info: Table b9e4d082-67fd-48d8-91bf-69a3f8b056fe: This server is Raft leader for term 1. Latest log index is 0. 74 | 2015-12-12T18:43:33.163129000 48208.219909s info: Table b9e4d082-67fd-48d8-91bf-69a3f8b056fe: Configuration is changing. 75 | -------------------------------------------------------------------------------- /rethinkdb_data/metadata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/incrediblesound/nebulaDB/496dae2be91da2a061725383480dcf36216d66c0/rethinkdb_data/metadata -------------------------------------------------------------------------------- /src/createDatabase.js: -------------------------------------------------------------------------------- 1 | var r = require('rethinkdb'); 2 | 3 | function createDatabase(name){ 4 | r.connect({host: 'localhost', port: 28015}).then(function(conn){ 5 | return r.dbCreate(name).run(conn) 6 | }).then(function(){ 7 | r.connect({host: 'localhost', port: 28015, db: name}).then(function(conn){ 8 | return r.tableCreate('data').run(conn); 9 | }).then(function(){ 10 | r.connect({host: 'localhost', port: 28015, db: name}).then(function(conn){ 11 | return r.table('data').insert({id: 0, count: 0}).run(conn); 12 | }).then(function(){ 13 | constructIndex(name); 14 | }) 15 | }) 16 | }) 17 | } 18 | 19 | function constructIndex(name){ 20 | r.connect({host: 'localhost', port: 28015, db: name}).then(function(conn){ 21 | return r.table('data').indexCreate('data').run(conn); 22 | }).then(function(){ 23 | console.log('Database creation complete.') 24 | }); 25 | } 26 | 27 | module.exports = createDatabase; 28 | -------------------------------------------------------------------------------- /src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | exports.processValue = processValue; 2 | exports.splitQuery = splitQuery; 3 | 4 | function isNumber(x){ 5 | if(parseInt(x) !== parseInt(x)){ 6 | return false; 7 | } 8 | return true; 9 | } 10 | 11 | function processValue(val){ 12 | if(isNumber(val)){ 13 | return parseInt(val); 14 | } else { 15 | return val; 16 | } 17 | } 18 | 19 | function splitQuery(query){ 20 | if(query.length === 3){ 21 | return query; 22 | } else { 23 | query = query[0].split(' '); 24 | return query; 25 | } 26 | } -------------------------------------------------------------------------------- /src/lib/lib.js: -------------------------------------------------------------------------------- 1 | exports.valueToType = { 2 | 'string':'s', 3 | 'number':'i' 4 | }; 5 | exports.relationToType = function(relation){ 6 | var lib = { 7 | '->':'e', //equals 8 | '-!':'n' //not equals 9 | }; 10 | return lib[relation] !== undefined ? lib[relation] : 'c' //custom 11 | }; 12 | 13 | exports.relationToFunc = { 14 | '->':'has_state', 15 | '-!':'not_has_state' 16 | }; -------------------------------------------------------------------------------- /src/lib/set.js: -------------------------------------------------------------------------------- 1 | var Set = function(array){ 2 | this.data = array || []; 3 | }; 4 | Set.prototype.contains = function(item){ 5 | return (this.data && this.data.length) ? (this.data.indexOf(item) > -1) : false; 6 | }; 7 | Set.prototype.get = function(idx){ 8 | return this.data[idx]; 9 | }; 10 | Set.prototype.setData = function(array){ 11 | this.data = array; 12 | }; 13 | Set.prototype.append = function(set){ 14 | return new Set(this.data.concat(set.data)); 15 | }; 16 | Set.prototype.add = function(item){ 17 | this.data.push(item); 18 | }; 19 | Set.prototype.rnd = function(){ 20 | var index = Math.floor(Math.random()*this.data.length); 21 | return this.get(index); 22 | } 23 | Set.prototype.isEmpty = function(){ 24 | return this.data.length === 0; 25 | } 26 | 27 | Set.prototype.print = function(){ 28 | for(var i = 0; i < this.data.length; i++){ 29 | console.log(this.data[i]) 30 | } 31 | } 32 | 33 | module.exports = { 34 | Set: Set, 35 | } -------------------------------------------------------------------------------- /src/nebuladb.js: -------------------------------------------------------------------------------- 1 | var r = require('rethinkdb'); 2 | var _ = require('lodash'); 3 | _ = _.extend(_, require('./lib/helpers.js')); 4 | var write = require('./write.js'); 5 | var read = require('./read.js'); 6 | 7 | var DB = function(){ 8 | this.index = 1; 9 | this.interval; 10 | }; 11 | 12 | DB.prototype.init = function(name, cb){ 13 | this.connection = {host: 'localhost', port: 28015, db: name}; 14 | var self = this; 15 | return r.connect(this.connection).then(function(conn){ 16 | return r.table('data').get(0).run(conn); 17 | }).then(function(result){ 18 | self.index = result.count; 19 | return cb(); 20 | }) 21 | } 22 | 23 | DB.prototype.save = function(query){ 24 | var query = _.splitQuery(query); 25 | write.writeEntry(query, this); 26 | } 27 | 28 | DB.prototype.saveAll = function(array){ 29 | var self = this; 30 | _.forEach(array, function(query){ 31 | self.save(query); 32 | }) 33 | } 34 | 35 | DB.prototype.query = function(query, cb){ 36 | var query = _.splitQuery(query); 37 | this.process_query(query, cb); 38 | } 39 | 40 | DB.prototype.removeLink = function(query){ 41 | var query = _.splitQuery(query); 42 | record.removeLink(query, this); 43 | } 44 | 45 | DB.prototype.process_query = function(query, cb){ 46 | if(query[0] === '*'){ 47 | if(query[1] === '*'){ 48 | read.allIncoming(query[2], this, cb); 49 | } 50 | else if(query[1] !== '->'){ 51 | read.customIncoming(query[2], query[1], this, cb); 52 | } else { 53 | read.incomingSimple(query[2], this, cb); 54 | } 55 | } 56 | else if(query[2] === '*'){ 57 | if(query[1] === '*'){ 58 | read.allOutgoing(query[0], this, cb); 59 | } 60 | else if(query[1] !== '->'){ 61 | read.customOutgoing(query[0], query[1], this, cb); 62 | } else { 63 | read.outgoingSimple(query[0], this, cb); 64 | } 65 | } 66 | else if(query[1] !== '->'){ 67 | read.checkCustomRelation(query, this, cb); 68 | } else { 69 | read.checkSimpleRelation([query[0],query[2]], this, cb); 70 | } 71 | } 72 | 73 | var nebuladb = { 74 | create: function(name, cb){ 75 | var db = new DB(); 76 | db.init(name, function(){ 77 | cb(db); 78 | }); 79 | } 80 | }; 81 | 82 | module.exports = nebuladb; 83 | -------------------------------------------------------------------------------- /src/read.js: -------------------------------------------------------------------------------- 1 | var r = require('rethinkdb'); 2 | var _ = require('lodash'); 3 | 4 | function outgoingSimple(source, self, cb){ 5 | getAll(self, [source], 'data', function(err, result){ 6 | if(!result.length){ return cb([]); } 7 | 8 | var record = result[0]; 9 | if(!record.out.length){ return cb([]); } 10 | 11 | getAll(self, record.out, null, function(err, results){ 12 | 13 | results = _.map(results, function(item){ 14 | return item.data 15 | }) 16 | cb(results) 17 | 18 | }) 19 | }) 20 | } 21 | 22 | function _outgoingCustom(source, self, cb){ 23 | 24 | getAll(self, [source], 'data', function(err, source){ 25 | var source = source[0]; 26 | if(!source.customOut.length){ return cb([]); } 27 | 28 | getAll(self, source.customOut, null, function(err, links){ 29 | 30 | var items = _.map(links, function(item){ return {id: item.id, data: item.data } }); 31 | var targetIDs = _.map(links, function(item){ return item.mapOut[source.id] }); 32 | targetIDs = _.flatten(targetIDs); 33 | if(!targetIDs.length){ return cb([]); } 34 | 35 | getAll(self, targetIDs, null, function(err, targets){ 36 | var result = {}; 37 | 38 | _.map(targets, function(target, i){ 39 | var parent = _.filter(items, function(item){ 40 | return _.contains(target.customIn, item.id); 41 | }) 42 | parent = parent[0]; 43 | result[parent.data] = result[parent.data] || []; 44 | result[parent.data].push(target.data); 45 | }) 46 | 47 | cb(result); 48 | }) 49 | }) 50 | }) 51 | } 52 | 53 | function _incomingCustom(target, self, cb){ 54 | 55 | getAll(self, [target], 'data', function(err, target){ 56 | var target = target[0]; 57 | if(!target.customIn.length){ cb([]); } 58 | 59 | getAll(self, target.customIn, null, function(err, links){ 60 | 61 | var items = _.map(links, function(item){ return {id: item.id, data: item.data } }); 62 | var targetIDs = _.map(links, function(item){ return item.mapIn[target.id] }); 63 | targetIDs = _.flatten(targetIDs); 64 | if(!targetIDs.length){ return cb([]); } 65 | 66 | getAll(self, targetIDs, null, function(err, sources){ 67 | var result = {}; 68 | 69 | _.map(sources, function(source, i){ 70 | var parent = _.filter(items, function(item){ 71 | return _.contains(source.customOut, item.id); 72 | }) 73 | parent = parent[0]; 74 | result[parent.data] = result[parent.data] || []; 75 | result[parent.data].push(source.data); 76 | }) 77 | 78 | cb(result); 79 | }) 80 | }) 81 | }) 82 | } 83 | 84 | function incomingSimple(target, self, cb){ 85 | getAll(self, [target], 'data', function(err, result){ 86 | 87 | if(!result.length){ return cb([]); } 88 | 89 | var record = result[0]; 90 | if(!record.in.length){ return cb([]); } 91 | 92 | getAll(self, record.in, null, function(err, results){ 93 | 94 | results = _.map(results, function(item){ 95 | return item.data 96 | }) 97 | cb(results); 98 | 99 | }) 100 | }) 101 | } 102 | 103 | function customOutgoing(source, link, self, cb){ 104 | var query = [source, link]; 105 | getAll(self, query.slice(), 'data', function(err, result){ 106 | if(err || result.length < 2){ cb([]); } 107 | var source = _.findWhere(result, {data: query[0]}); 108 | var link = _.findWhere(result, {data: query[1]}); 109 | if(!link.mapOut[source.id] || !link.mapOut[source.id].length){ 110 | return cb([]); 111 | } 112 | getAll(self, link.mapOut[source.id], null, function(err, result){ 113 | result = _.map(result, function(item){ 114 | return item.data; 115 | }) 116 | cb(result); 117 | }) 118 | }) 119 | } 120 | 121 | function customIncoming(target, link, self, cb){ 122 | var query = [target, link]; 123 | getAll(self, query.slice(), 'data', function(err, result){ 124 | if(err || result.length < 2){ cb([]); } 125 | var target = _.findWhere(result, {data: query[0]}); 126 | var link = _.findWhere(result, {data: query[1]}); 127 | 128 | getAll(self, link.mapIn[target.id], null, function(err, result){ 129 | result = _.map(result, function(item){ 130 | return item.data; 131 | }) 132 | cb(result); 133 | }) 134 | }) 135 | } 136 | 137 | function checkCustomRelation(query, self, cb){ 138 | getAll(self, query, 'data', function(err, result){ 139 | if(result.length < 3){ return cb(null); } 140 | 141 | var source = _.findWhere(result, { data: query[0] }); 142 | var link = _.findWhere(result, { data: query[1] }); 143 | var target = _.findWhere(result, { data: query[2] }); 144 | var hasRelation = _.contains(link.mapOut[source.id], target.id); 145 | cb(hasRelation); 146 | }) 147 | } 148 | 149 | function checkSimpleRelation(query, self, cb){ 150 | getAll(self, query, 'data', function(err, result){ 151 | if(result.length < 2){ return cb(null); } 152 | 153 | var source = _.findWhere(result, { data: query[0] }); 154 | var target = _.findWhere(result, { data: query[1] }); 155 | 156 | var hasRelation = _.contains(source.out, target.id); 157 | cb(hasRelation); 158 | }) 159 | } 160 | 161 | function allOutgoing(source, self, cb){ 162 | var counter = 0, finalResult = {}; 163 | 164 | outgoingSimple(source, self, _.partial(collectResults, 'simple')); 165 | _outgoingCustom(source, self, _.partial(collectResults, 'custom')); 166 | 167 | function collectResults(type, results){ 168 | if(type === 'custom'){ 169 | finalResult['custom'] = results; 170 | counter++; 171 | } else if(type === 'simple'){ 172 | finalResult['simple'] = results; 173 | counter++; 174 | } 175 | if(counter === 2){ 176 | cb(finalResult); 177 | } 178 | } 179 | } 180 | 181 | function allIncoming(target, self, cb){ 182 | var counter = 0, finalResult = {}; 183 | 184 | incomingSimple(target, self, _.partial(collectResults, 'simple')); 185 | _incomingCustom(target, self, _.partial(collectResults, 'custom')); 186 | 187 | function collectResults(type, results){ 188 | if(type === 'custom'){ 189 | finalResult['custom'] = results; 190 | counter++; 191 | } else if(type === 'simple'){ 192 | finalResult['simple'] = results; 193 | counter++; 194 | } 195 | if(counter === 2){ 196 | cb(finalResult); 197 | } 198 | } 199 | } 200 | 201 | function getAll(self, ids, index, cb){ 202 | if(index !== null){ 203 | r.connect(self.connection).then(function(conn){ 204 | return r.table('data').getAll(r.args(ids), {index: index}).run(conn); 205 | }).then(function(cursor){ 206 | cursor.toArray(cb); 207 | }) 208 | } else { 209 | r.connect(self.connection).then(function(conn){ 210 | return r.table('data').getAll(r.args(ids)).run(conn); 211 | }).then(function(cursor){ 212 | cursor.toArray(cb); 213 | }) 214 | } 215 | 216 | } 217 | 218 | module.exports = { 219 | allOutgoing: allOutgoing, 220 | allIncoming: allIncoming, 221 | customOutgoing: customOutgoing, 222 | customIncoming: customIncoming, 223 | outgoingSimple: outgoingSimple, 224 | incomingSimple: incomingSimple, 225 | checkSimpleRelation: checkSimpleRelation, 226 | checkCustomRelation: checkCustomRelation 227 | } 228 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var nebulaDB = require('./nebuladb.js'); 3 | 4 | module.exports = function(dbName){ 5 | nebulaDB.create(dbName, function(DB){ 6 | var port = 1984; 7 | var ip = '127.0.0.1'; 8 | 9 | var server = http.createServer(requestHandler); 10 | var headers = { 11 | "access-control-allow-origin": "*", 12 | "access-control-allow-methods": "POST", 13 | "access-control-allow-headers": "content-type, accept", 14 | "access-control-max-age": 60 // Seconds. 15 | }; 16 | 17 | server.listen(port, ip); 18 | console.log('NebulaDB listening on port '+port); 19 | 20 | function requestHandler(req, res){ 21 | res.writeHead(200, headers); 22 | retrieveData(req, function(data){ 23 | if(req.url === '/save'){ 24 | DB.save(data); 25 | res.end(); 26 | } 27 | else if(req.url === '/saveall'){ 28 | DB.saveAll(data); 29 | res.end(); 30 | } 31 | else if(req.url === '/query'){ 32 | DB.query(data, function(result){ 33 | res.end(JSON.stringify(result)); 34 | }) 35 | } 36 | }) 37 | } 38 | }) 39 | 40 | } 41 | 42 | function retrieveData(request, cb){ 43 | request.on('data', function(chunk) { 44 | chunk = chunk.toString(); 45 | chunk = JSON.parse(chunk); 46 | cb(chunk); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/write.js: -------------------------------------------------------------------------------- 1 | var r = require('rethinkdb'); 2 | var _ = require('lodash'); 3 | 4 | const SOURCE = 0 5 | const TARGET = 2 6 | 7 | const QUERY = function(){ 8 | return { data: '', in: [], out: [], customIn: [], customOut: [], mapOut: {}, mapIn: {} }; 9 | } 10 | 11 | function writeEntry(query, self, cb){ /// key, 1,2,3<>6,7 IN/OUT 12 | if(query[1] === '->'){ 13 | writeSimpleRelation(query, self, cb); 14 | } else { 15 | writeCustomRelation(query, self, cb); 16 | } 17 | } 18 | 19 | function writeSimpleRelation(query, self, cb){ 20 | r.connect(self.connection) 21 | .then(function(conn){ 22 | return r.table('data').getAll(query[0],query[2], {index: "data"}).run(conn); 23 | }).then(function(cursor){ 24 | cursor.toArray(function(err, results){ 25 | if(!results.length){ 26 | writeRecord(self, { id: self.index, data: query[0], out: [self.index+1]}) 27 | .then(_.partial(writeRecord, self, { id: self.index+1, data: query[2], in: [self.index] })) 28 | .then(_.partial(updateIndex, self, 2, cb)); 29 | } else if(results.length === 1) { 30 | var result = results[0], record; 31 | if(result.data === query[0]){ 32 | writeRecord(self, { data: query[2], in: [result.id], id: self.index }) 33 | .then(_.partial(addOutgoing, self, result.id, self.index)) 34 | .then(_.partial(updateIndex, self, 1, cb)); 35 | } else { 36 | writeRecord(self, { data: query[0], out: [result.id], id: self.index}) 37 | .then(_.partial(addIncoming, self, result.id, self.index)) 38 | .then(_.partial(updateIndex, self, 1, cb)); 39 | } 40 | } else { 41 | cb(); 42 | } 43 | }) 44 | }) 45 | } 46 | 47 | function writeCustomRelation(query, self, cb){ 48 | r.connect(self.connection) 49 | .then(function(conn){ 50 | return r.table('data') 51 | .getAll(query[0], query[1], query[2], {index: "data"}) 52 | .run(conn); 53 | }) 54 | .then(function(cursor){ 55 | var mapOut = {}; 56 | var mapIn = {}; 57 | cursor.toArray(function(err, results){ 58 | var source = _.findWhere(results, {data: query[0]}); 59 | var link = _.findWhere(results, {data: query[1]}); 60 | var target = _.findWhere(results, {data: query[2]}); 61 | 62 | if(!results.length){ 63 | mapOut[self.index] = [self.index+2]; 64 | mapIn[self.index+2] = [self.index]; 65 | writeRecord(self, { id: self.index, data: query[0], customOut: [self.index+1]}) 66 | .then(_.partial(writeRecord, self, { id: self.index+1, data: query[1], mapOut: mapOut, mapIn: mapIn })) 67 | .then(_.partial(writeRecord, self, { id: self.index+2, data: query[2], customIn: [self.index+1] })) 68 | .then(_.partial(updateIndex, self, 3, cb)); 69 | 70 | } else if(results.length === 3){ 71 | addCustomOut(self, source.id, link.id) 72 | .then(_.partial(addThroughLink, self, link.id, source.id, target.id)) 73 | .then(_.partial(addCustomIn, self, target.id, link.id)) 74 | .then(cb) 75 | 76 | } else { 77 | // at least one missing 78 | if(source !== undefined){ 79 | if(link !== undefined){ 80 | // no target +link +source 81 | writeRecord(self, {id: self.index, data: query[2], customIn: [link.id]}) 82 | .then(_.partial(addThroughLink, self, link.id, source.id, self.index)) 83 | .then(_.partial(addCustomOut, self, source.id, link.id, self.index)) 84 | .then(_.partial(updateIndex, self, 1, cb)) 85 | } else { 86 | if(target !== undefined){ 87 | // no link +source +target 88 | mapOut[source.id] = [target.id]; 89 | mapIn[target.id] = [source.id]; 90 | writeRecord(self, {id: self.index, data: query[1], mapIn: mapIn, mapOut: mapOut}) 91 | .then(_.partial(addCustomOut, self, source.id, self.index, target.id)) 92 | .then(_.partial(addCustomIn, self, target.id, self.index, source.id)) 93 | .then(_.partial(updateIndex, self, 1, cb)) 94 | } else { 95 | // no link no target +source 96 | mapOut[source.id] = [self.index+1]; 97 | mapIn[self.index+1] = [source.id]; 98 | writeRecord(self, {id: self.index, data: query[1], mapOut: mapOut, mapIn: mapIn }) 99 | .then(_.partial(writeRecord, self, {id: self.index+1, data: query[2], customIn: [self.index]})) 100 | .then(_.partial(addCustomOut, self, source.id, self.index, self.index+1)) 101 | .then(_.partial(updateIndex, self, 2, cb)) 102 | } 103 | } 104 | } else { 105 | // no source 106 | if(link !== undefined){ 107 | if(target !== undefined){ 108 | // no source +link +target 109 | writeRecord(self, {id: self.index, data: query[0], customOut: [link.id]}) 110 | .then(_.partial(addThroughLink, self, link.id, self.index, target.id)) 111 | .then(_.partial(addCustomIn, self, target.id, link.id, self.index)) 112 | .then(_.partial(updateIndex, self, 1, cb)) 113 | } else { 114 | // no source no target +link 115 | writeRecord(self, {id: self.index, data: query[0], customOut: [link.id] }) 116 | .then(_.partial(writeRecord, self, {id: self.index+1, data: query[2], customIn: [link.id]})) 117 | .then(addThroughLink, self, link.id, self.index, self.index+1) 118 | .then(_.partial(updateIndex, self, 2, cb)) 119 | } 120 | } else { 121 | // no source no link +target 122 | mapOut[self.index] = target.id; 123 | mapIn[target.id] = self.index; 124 | writeRecord(self, {id: self.index, data: query[0], customOut: [self.index+1] }) 125 | .then(_.partial(writeRecord, self, {id: self.index+1, data: query[1], mapIn: mapIn, mapOut: mapOut})) 126 | .then(_.partial(addCustomIn, self, target.id, self.index+1, self.index)) 127 | .then(_.partial(updateIndex, self, 2, cb)) 128 | } 129 | 130 | } 131 | } 132 | }) 133 | }) 134 | } 135 | 136 | /* 137 | * Utility functions for writing records 138 | */ 139 | 140 | function writeRecord(self, record){ 141 | record = _.extend(QUERY(), record); 142 | return r.connect(self.connection).then(function(conn){ 143 | return r.table('data').insert(record).run(conn); 144 | }) 145 | } 146 | 147 | function addCustomIn(self, target, link, source){ 148 | return r.connect(self.connection).then(function(conn){ 149 | return r.table('data').get(target).update({ 150 | customIn: r.row('customIn').setInsert(link) 151 | }).run(conn) 152 | }) 153 | } 154 | function addCustomOut(self, source, link, target){ 155 | return r.connect(self.connection).then(function(conn){ 156 | return r.table('data').get(source).update({ 157 | customOut: r.row('customOut').setInsert(link) 158 | }).run(conn) 159 | }) 160 | } 161 | function addThroughLink(self, link, source, target){ 162 | return r.connect(self.connection).then(function(conn){ 163 | return r.table('data') 164 | .get(link) 165 | .update(function(link){ 166 | return { 167 | mapOut: link("mapOut").merge({[source]: link('mapOut')(source.toString()).default([]).setInsert(target) }), 168 | mapIn: link("mapIn").merge({[target]: link('mapIn')(target.toString()).default([]).setInsert(source) }) 169 | } 170 | }).run(conn); 171 | }) 172 | } 173 | 174 | function updateIndex(self, n, cb){ 175 | self.index += n; 176 | return r.connect(self.connection).then(function(conn){ 177 | return r.table('data').get(0).update({ count: self.index }).run(conn); 178 | }).then(cb) 179 | } 180 | 181 | function addOutgoing(self, recordId, idToAdd){ 182 | return r.connect(self.connection).then(function(conn){ 183 | return r.table('data') 184 | .get(recordId) 185 | .update({ out: r.row('out').setInsert(idToAdd) }) 186 | .run(conn); 187 | }) 188 | } 189 | 190 | function addIncoming(self, recordId, idToAdd){ 191 | return r.connect(self.connection).then(function(conn){ 192 | return r.table('data') 193 | .get(recordId) 194 | .update({ in: r.row('in').setInsert(idToAdd) }) 195 | .run(conn); 196 | }) 197 | } 198 | 199 | 200 | module.exports = { 201 | writeEntry: writeEntry, 202 | }; 203 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 2 | var nebulaDB = require('./src/nebuladb.js'); 3 | nebulaDB.create('nebula', function(db){ 4 | 5 | // db.save(['name -> attr']); 6 | // db.save(['tony -> user']); 7 | // db.save(['tony -> admin']); 8 | // db.save(['tony','name', 'Tony Baloney']); 9 | // db.save(['tony','speciality', 'Security']); 10 | // db.save(['tony','speciality', 'Software']); 11 | // db.save(['tony','speciality', 'HR']); 12 | // db.save(['tony','phone','111-222-3333']); 13 | // db.save(['tony','phone', '111 222 3333']); 14 | 15 | // db.save(['person1','name','james']) 16 | // db.save(['person2','name','james']) 17 | // db.save(['person3','last_name','james']) 18 | // db.save(['first_name','->','james']) 19 | // db.save(['good_name','->','james']) 20 | 21 | db.query(['*','->','user'],{ where: ['* name James'] }, function(result){ 22 | console.log(result); 23 | }); 24 | 25 | }); 26 | --------------------------------------------------------------------------------