├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── example └── expanded.js ├── index.js ├── package.json └── test └── groups-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | 12 | # OS or Editor folders 13 | .DS_Store 14 | .cache 15 | .project 16 | .settings 17 | nbproject 18 | thumbs.db 19 | 20 | # Logs 21 | .log 22 | .pid 23 | .sock 24 | .monitor 25 | 26 | # Dreamweaver added files 27 | _notes 28 | dwsync.xml 29 | 30 | # Komodo 31 | *.komodoproject 32 | .komodotools 33 | 34 | # Folders to ignore 35 | node_modules 36 | .hg 37 | .svn 38 | publish 39 | .idea 40 | _dev 41 | 42 | # build script local files 43 | build/buildinfo.properties 44 | build/config/buildinfo.properties 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012 Thomas Blobaum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redis-graph 2 | 3 | a powerful graph implementation using [redis sets](http://redis.io/commands#set) 4 | 5 | [![Build Status](https://secure.travis-ci.org/tblobaum/redis-graph.png)](http://travis-ci.org/tblobaum/redis-graph) 6 | 7 | # Methods 8 | 9 | `redis-graph` uses an instance of `node_redis` to connect to redis with 10 | 11 | ``` js 12 | 13 | var Node = require('redis-graph') 14 | Node.setClient(require('redis').createClient()) 15 | 16 | ``` 17 | 18 | ## Node(id [, opts]) 19 | return a new `node` 20 | 21 | An options argument with `inner` and `outer` properties can be passed as the second argument, which will be used for the inner/outer edges for the node. 22 | 23 | `inner` defaults to `'membership'` and `'outer'` defaults to `'members'` 24 | 25 | The defaults are suitable for a graph of groups. These edge names could be something else, e.g. `'followers'` and `'following'` to mimic twitter's social graph. 26 | 27 | Note: The `nodes` argument to all api calls below can either be a string id (e.g. `'user'`), an array of ids (e.g. `[ 'user', 'admin' ]`), an instance of `Node` or an array of instances of `Node` 28 | 29 | 30 | ``` js 31 | 32 | var Node = require('redis-graph') 33 | Node.setClient(require('redis').createClient()) 34 | 35 | var myGroup = Node('me') 36 | , yourGroup = Node('you') 37 | , adminGroup = Node('admin') 38 | , userGroup = Node('user') 39 | 40 | userGroup.members.add('me', function (error) { 41 | userGroup.members.add('you', function (erro) { 42 | myGroup.members.add('you', function (err) { 43 | yourGroup.members.add('me', function (er) { 44 | adminGroup.members.add('you', function (e) { 45 | // do something with the connections 46 | // .. 47 | 48 | }) 49 | }) 50 | }) 51 | }) 52 | }) 53 | 54 | ``` 55 | 56 | ## .{members,membership}.add(nodes, function (error) { ... }) 57 | add an edge or edges to `nodes` from this instance 58 | 59 | ``` js 60 | Node('user').members.add('me', function (error) { 61 | // .. 62 | }) 63 | 64 | // is exactly the same as 65 | Node('me').membership.add('user', function (error) { 66 | // .. 67 | }) 68 | ``` 69 | 70 | ## .{members,membership}.has(node, function (error, hasNode) { ... }) 71 | checks wether not this instance has an edge to `node` 72 | 73 | ``` js 74 | Node('user').members.has('me', function (error, hasUser) { 75 | // hasUser == true || hasUser == false 76 | }) 77 | ``` 78 | 79 | ## .{members,membership}.all(function (error, nodes) { ... }) 80 | return a list of nodes 81 | 82 | ``` js 83 | Node('user').members.all(function (err, nodes) { 84 | console.log(nodes) 85 | // => [ 'me' ] 86 | }) 87 | 88 | Node('me').membership.all(function (err, nodes) { 89 | console.log(nodes) 90 | // => [ 'user' ] 91 | }) 92 | ``` 93 | 94 | ## .{members,membership}.delete(nodes, function (error) { ... }) 95 | remove an edge or edges from `nodes` to this instance 96 | 97 | ``` js 98 | Node('user').members.delete('me', function (err, nodes) { 99 | console.log(nodes) 100 | // => [ 'me' ] 101 | }) 102 | 103 | // is exactly the same as 104 | Node('me').membership.delete('user', function (err, nodes) { 105 | console.log(nodes) 106 | // => [ 'user' ] 107 | }) 108 | ``` 109 | 110 | ## .{members,membership}.union(nodes, function (error, nodes) { ... }) 111 | return a union of the edges with the `nodes` edges provided with logical `||` 112 | 113 | ``` js 114 | Node('you').membership.union('me', function (e, nodes) { 115 | // do something with the list of memberships either 'you' || 'me' have 116 | // .. 117 | }) 118 | 119 | Node('user').members.union('admin', function (e, nodes) { 120 | // do something with the members of 'user' and 'admin' 121 | // .. 122 | }) 123 | ``` 124 | 125 | ## .{members,membership}.intersect(nodes, function (error, nodes) { ... }) 126 | return an intersection of the edges with the `nodes` edges provided, with logical `&&` 127 | 128 | ``` js 129 | Node('you').membership.intersect('me', function (e, nodes) { 130 | // do something with the list of memberships both 'you' && 'me' have 131 | // .. 132 | }) 133 | ``` 134 | 135 | ## .{members,membership}.without(nodes, function (error, nodes) { ... }) 136 | return the result of a subtraction of the `nodes` edges from the instance's edges 137 | 138 | ``` js 139 | Node('you').membership.without('me', function (e, nodes) { 140 | // do something with the list of memberships 'you' have that 'me' does not have 141 | // .. 142 | }) 143 | ``` 144 | 145 | ## .{members,membership}.{members,membership}(function (error, nodes) { ... }) 146 | return the nodes of the edges we have edges to! (similar to `all`, but another level deep) 147 | 148 | ``` js 149 | Node('you').membership.membership(function (e, nodes) { 150 | // do something with a list of nodes that have membership to the nodes that i have membership to 151 | // .. 152 | }) 153 | 154 | Node('you').membership.members(function (e, nodes) { 155 | // do something with a list of nodes that are members of the nodes that i have membership to 156 | // .. 157 | }) 158 | ``` 159 | 160 | ## .toString(function (error) { ... }) 161 | return the string id of this instance 162 | 163 | ``` js 164 | Node('user').toString() 165 | // => 'user' 166 | 167 | ``` 168 | 169 | ## .delete(function (error) { ... }) 170 | delete this instance and remove all of its edges from redis 171 | 172 | ``` js 173 | Node('user').delete(function (err) { 174 | // .. 175 | 176 | }) 177 | ``` 178 | 179 | # Example 180 | 181 | Check the examples directory for more stuff. 182 | 183 | ``` js 184 | var Node = require('./') 185 | Node.setClient(require('redis').createClient()) 186 | 187 | var myGroup = Node('me', { inner : 'membership', outer : 'members' }) 188 | var yourGroup = Node('you', { inner : 'membership', outer : 'members' }) 189 | var adminGroup = Node('admin', { inner : 'membership', outer : 'members' }) 190 | var userGroup = Node('user', { inner : 'membership', outer : 'members' }) 191 | 192 | userGroup.members.add('me', function (err) { 193 | userGroup.members.add('you', function (err) { 194 | myGroup.members.add('you', function (err) { 195 | yourGroup.members.add('me', function (err) { 196 | adminGroup.members.add('you', function (e) { 197 | 198 | // 199 | // Members 200 | // - having a member 201 | // 202 | 203 | userGroup.members.all(function (e, nodes) { 204 | console.log('err', e) 205 | console.log('members of "user":', nodes) 206 | // => [ 'you', 'me' ] 207 | }) 208 | 209 | myGroup.members.all(function (e, nodes) { 210 | console.log('err', e) 211 | console.log('members of "me":', nodes) 212 | // => [ 'you' ] 213 | }) 214 | 215 | yourGroup.members.all(function (e, nodes) { 216 | console.log('err', e) 217 | console.log('members of "you":', nodes) 218 | // => [ 'me' ] 219 | }) 220 | 221 | adminGroup.members.all(function (e, nodes) { 222 | console.log('err', e) 223 | console.log('members of "admin":', nodes) 224 | // => [ 'you' ] 225 | }) 226 | 227 | userGroup.members.union('admin', function (e, nodes) { 228 | console.log('err', e) 229 | console.log('members of "user" || "admin":', nodes) 230 | // => [ 'you', 'me' ] 231 | }) 232 | 233 | userGroup.members.intersect('admin', function (e, nodes) { 234 | console.log('err', e) 235 | console.log('members of both "user" && "admin":', nodes) 236 | // => [ 'you' ] 237 | }) 238 | 239 | userGroup.members.without('admin', function (e, nodes) { 240 | console.log('err', e) 241 | console.log('members of "user" without members of "admin":', nodes) 242 | // => [ 'me' ] 243 | }) 244 | 245 | userGroup.members.members(function (e, nodes) { 246 | console.log('err', e) 247 | console.log('members of the members of "user":', nodes) 248 | // => [ 'you', 'me' ] 249 | }) 250 | 251 | userGroup.members.membership(function (e, nodes) { 252 | console.log('err', e) 253 | console.log('membership of the members of "user":', nodes) 254 | // => [ 'user', 'you', 'admin', 'me' ] 255 | }) 256 | 257 | // 258 | // Membership 259 | // - being a member 260 | // 261 | 262 | userGroup.membership.all(function (e, nodes) { 263 | console.log('err', e) 264 | console.log('membership of "user":', nodes) 265 | // => [ ] 266 | }) 267 | 268 | myGroup.membership.all(function (e, nodes) { 269 | console.log('err', e) 270 | console.log('membership of "me":', nodes) 271 | // => [ 'user', 'you' ] 272 | }) 273 | 274 | yourGroup.membership.all(function (e, nodes) { 275 | console.log('err', e) 276 | console.log('membership of "you":', nodes) 277 | // => [ 'user', 'admin', 'me' ] 278 | }) 279 | 280 | adminGroup.membership.all(function (e, nodes) { 281 | console.log('err', e) 282 | console.log('membership of "admin":', nodes) 283 | // => [ ] 284 | }) 285 | 286 | yourGroup.membership.union('me', function (e, nodes) { 287 | console.log('err', e) 288 | console.log('membership of either "you" || "me":', nodes) 289 | // => [ 'user', 'you', 'me', 'admin' ] 290 | }) 291 | 292 | yourGroup.membership.intersect('me', function (e, nodes) { 293 | console.log('err', e) 294 | console.log('membership of "you" && "me":', nodes) 295 | // => [ 'user' ] 296 | }) 297 | 298 | yourGroup.membership.without('me', function (e, nodes) { 299 | console.log('err', e) 300 | console.log('membership of "you" without "me":', nodes) 301 | // => [ 'me', 'admin' ] 302 | }) 303 | 304 | yourGroup.membership.membership(function (e, nodes) { 305 | console.log('err', e) 306 | console.log('membership of the membership of "you":', nodes) 307 | // => [ 'user', 'you' ] 308 | }) 309 | 310 | yourGroup.membership.members(function (e, nodes) { 311 | console.log('err', e) 312 | console.log('members of the membership of "you":', nodes) 313 | // => [ 'you', 'me' ] 314 | }) 315 | 316 | }) 317 | }) 318 | }) 319 | }) 320 | }) 321 | 322 | ``` 323 | 324 | # Install 325 | 326 | `npm install redis-graph` 327 | 328 | # Tests 329 | 330 | With redis running locally do: 331 | 332 | `npm install -g tap && npm test` 333 | 334 | # License 335 | 336 | (The MIT License) 337 | 338 | Copyright (c) 2012 Thomas Blobaum 339 | 340 | Permission is hereby granted, free of charge, to any person obtaining 341 | a copy of this software and associated documentation files (the 342 | 'Software'), to deal in the Software without restriction, including 343 | without limitation the rights to use, copy, modify, merge, publish, 344 | distribute, sublicense, and/or sell copies of the Software, and to 345 | permit persons to whom the Software is furnished to do so, subject to 346 | the following conditions: 347 | 348 | The above copyright notice and this permission notice shall be 349 | included in all copies or substantial portions of the Software. 350 | 351 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 352 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 353 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 354 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 355 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 356 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 357 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 358 | -------------------------------------------------------------------------------- /example/expanded.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | , client = require('redis').createClient() 3 | , group = require('../') 4 | 5 | group.setClient(client) 6 | 7 | var tom = group('tom') 8 | , bob = group('bob') 9 | , sarah = group('sarah') 10 | , nodejs = group('nodejs') 11 | , bash = group('bash') 12 | , javascript = group('javascript') 13 | 14 | // 15 | // Member and membership control 16 | // 17 | 18 | tom.membership.add([ 'nodejs', 'bash', 'javascript' ], log()) 19 | bob.membership.add('javascript', log()) 20 | sarah.membership.add('javascript', log()) 21 | nodejs.members.add(javascript, log()) 22 | // javascript.membership.add(nodejs, log()) 23 | 24 | nodejs.members.all(log('members of nodejs:')) 25 | javascript.members.all(log('members of javascript:')) 26 | bash.members.all(log('members of bash:')) 27 | tom.members.all(log('members of tom:')) 28 | bob.members.all(log('members of bob:')) 29 | sarah.members.all(log('members of sarah:')) 30 | 31 | nodejs.membership.all(log('nodejs membership:')) 32 | javascript.membership.all(log('javascript membership:')) 33 | bash.membership.all(log('bash membership:')) 34 | tom.membership.all(log('tom membership:')) 35 | bob.membership.all(log('bob membership:')) 36 | sarah.membership.all(log('sarah membership:')) 37 | 38 | // intersections can be used to gain new information about the graph. 39 | // As an example, you can find a subsets of groups in various ways: 40 | 41 | bob.membership.without('tom', log('bob membership without tom:')) 42 | bob.membership.intersect('tom', log('bob membership intersected with tom:')) 43 | javascript.membership.intersect('tom', log('javascript members intersected with tom:')) 44 | 45 | nodejs.members.without('bash', log('nodejs members without bash:')) 46 | nodejs.members.intersect('bash', log('nodejs members intersected with bash:')) 47 | 48 | // 49 | // Combining two groups 50 | // 51 | // tom and bob 52 | // 53 | 54 | // get the groups that *both* tom and bob can membership (AND) 55 | tom.membership.intersect(bob, log('memberships of tom && bob')) 56 | 57 | // get the groups that *either* tom or bob can membership (XOR) 58 | tom.membership.union(bob, log('memberships of tom || bob')) 59 | 60 | // get the membership that tom has that bob does not have 61 | tom.membership.without(bob, log('memberships of tom that bob does not have')) 62 | // => [ 'admin' ] 63 | 64 | // 65 | // Combining many groups 66 | // 67 | // tom, bob and sarah 68 | // 69 | 70 | // get the groups that each of tom, bob, 71 | // and sarah has membership to (AND) 72 | tom.membership.intersect([ bob, sarah ], log('memberships of tom && bob && sarah')) 73 | 74 | // get the groups that at least one of tom, 75 | // bob, or sarah has membership to (XOR) 76 | tom.membership.union([ bob, sarah ], log('memberships of tom || bob || sarah')) 77 | 78 | // get the groups that tom has membership to 79 | // that sarah and bob do not have membership to 80 | tom.membership.without([ bob, sarah ], log('memberships of tom !== sarah && memberships of tom !== bob')) 81 | 82 | // delete nodes and all connections 83 | tom.delete(log()) 84 | bob.delete(log()) 85 | sarah.delete(log()) 86 | nodejs.delete(log()) 87 | bash.delete(log()) 88 | javascript.delete(log()) 89 | 90 | function log (str) { 91 | return function (error, result) { 92 | var args = Array.prototype.slice.call(arguments) 93 | args[0] = (str || '') 94 | console.log.apply(console, args) 95 | assert.strictEqual(error, null, 'errors should be null') 96 | } 97 | } 98 | 99 | // client.end() 100 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var util = require('util') 2 | , format = util.format.bind(util) 3 | , db 4 | 5 | /** 6 | * Return a new Node instance with `node` and `opts` 7 | * 8 | * @param {String || Object} node 9 | * @param {Object} opts 10 | * @return {Object} 11 | * @api public 12 | */ 13 | 14 | module.exports = function (node, opts) { 15 | opts = opts || {} 16 | opts.inner = opts.inner || 'membership' 17 | opts.outer = opts.outer || 'members' 18 | return new Node(String(node), opts) 19 | } 20 | 21 | /** 22 | * Set the redis client 23 | * 24 | * @param {Object} client 25 | * @api public 26 | */ 27 | 28 | module.exports.setClient = function (client) { 29 | db = client 30 | } 31 | 32 | /** 33 | * Node Constructor 34 | */ 35 | 36 | function Node (id, opts) { 37 | var me = this 38 | this.id = id 39 | Object.defineProperty(me, 'inner', { 40 | value : opts.inner 41 | , enumerable : false 42 | , writable : true 43 | }) 44 | Object.defineProperty(me, 'outer', { 45 | value : opts.outer 46 | , enumerable : false 47 | , writable : true 48 | }) 49 | Object.defineProperty(me, this.inner, { 50 | value : new Edge({ inner : this.inner, outer : this.outer, id : id }) 51 | , enumerable : false 52 | , writable : true 53 | }) 54 | Object.defineProperty(me, this.outer, { 55 | value : new Edge({ inner : this.outer, outer : this.inner, id : id }) 56 | , enumerable : false 57 | , writable : true 58 | }) 59 | } 60 | 61 | /** 62 | * Return the string id of the node instance 63 | * 64 | * @return {String} 65 | * @api public 66 | */ 67 | 68 | Node.prototype.toString = function () { 69 | return this.id 70 | } 71 | 72 | /** 73 | * Delete the node and all of its edges 74 | * 75 | * @param {Function} cb 76 | * @api public 77 | */ 78 | 79 | Node.prototype.delete = function (cb) { 80 | var me = this 81 | db.multi() 82 | .smembers([ me[me.inner].innerkey ]) 83 | .smembers([ me[me.outer].innerkey ]) 84 | .exec(function (error, replies) { 85 | var multi = db.multi() 86 | replies.forEach(function (reply) { 87 | reply.forEach(function (gid) { 88 | multi 89 | .srem([ format(me.inner + '_%s', gid), me.id ]) 90 | .srem([ format(me.outer + '_%s', gid), me.id ]) 91 | .srem([ me[me.inner].innerkey, gid ]) 92 | .srem([ me[me.outer].innerkey, gid ]) 93 | }) 94 | }) 95 | multi.exec(cb) 96 | }) 97 | ; 98 | } 99 | 100 | /** 101 | * Initialize a new `Edge` with inner/outer edge names 102 | * 103 | * @param {Object} opts 104 | */ 105 | 106 | function Edge (opts) { 107 | this.id = opts.id 108 | this.innerformat = opts.inner + '_%s' 109 | this.outerformat = opts.outer + '_%s' 110 | this.innerkey = format(this.innerformat, this.id) 111 | this.outerkey = format(this.outerformat, this.id) 112 | this[opts.inner] = function (cb) { 113 | this.all(function (error, array) { 114 | if (error) return cb(error) 115 | if (!array || !array.length) return cb(null, array || []) 116 | array = array.map(function (gid) { return format(this.innerformat, String(gid)) }, this) 117 | db.sunion(array, cb) 118 | }.bind(this)) 119 | } 120 | this[opts.outer] = function (cb) { 121 | this.all(function (error, array) { 122 | if (error) return cb(error) 123 | if (!array || !array.length) return cb(null, array || []) 124 | array = array.map(function (gid) { return format(this.outerformat, gid) }, this) 125 | db.sunion(array, cb) 126 | }.bind(this)) 127 | } 128 | } 129 | 130 | /** 131 | * Return all of the inner edges for the parent `node` instance 132 | * 133 | * @param {Function} cb 134 | */ 135 | 136 | Edge.prototype.all = function (cb) { 137 | db.smembers([ this.innerkey ], cb) 138 | } 139 | 140 | /** 141 | * Add edges to `arr` for the parent `node` instance 142 | * 143 | * @param {String || Array} arr 144 | * @param {Function} cb 145 | */ 146 | 147 | Edge.prototype.add = function (arr, cb) { 148 | arr = Array.isArray(arr) ? arr : [ arr ] 149 | arr = arr.map(String) 150 | var multi = db.multi() 151 | arr.forEach(function (gid) { 152 | multi.sadd([ this.innerkey, String(gid) ]) 153 | multi.sadd([ format(this.outerformat, String(gid)), this.id ]) 154 | }, this) 155 | multi.exec(cb) 156 | } 157 | 158 | /** 159 | * Checks if parent `node` instance has an edge to `member` 160 | * 161 | * @param {String} member 162 | * @param {Function} cb 163 | */ 164 | 165 | Edge.prototype.has = function (member, cb) { 166 | db.sismember(this.innerkey, member, cb) 167 | } 168 | 169 | /** 170 | * Delete edges to `arr` for the parent `node` instance 171 | * 172 | * @param {String || Array} arr 173 | * @param {Function} cb 174 | */ 175 | 176 | Edge.prototype.delete = function (arr, cb) { 177 | arr = Array.isArray(arr) ? arr : [ arr ] 178 | arr = arr.map(String) 179 | var multi = db.multi() 180 | arr.forEach(function (gid) { 181 | multi.srem([ this.innerkey, String(gid) ]) 182 | multi.srem([ format(this.outerformat, String(gid)), this.id ]) 183 | }, this) 184 | multi.exec(cb) 185 | } 186 | 187 | /** 188 | * Return the inner edges with the edges of `arr` removed 189 | * 190 | * @param {String || Array} arr 191 | * @param {Function} cb 192 | */ 193 | 194 | Edge.prototype.without = function (arr, cb) { 195 | arr = Array.isArray(arr) ? arr : [ arr ] 196 | arr = arr.map(function (gid) { return format(this.innerformat, String(gid)) }, this) 197 | arr.unshift(this.innerkey) 198 | db.sdiff(arr, cb) 199 | } 200 | 201 | /** 202 | * Return an intersection (logical AND) of inner edges with those of `arr` 203 | * 204 | * @param {String || Array} arr 205 | * @param {Function} cb 206 | */ 207 | 208 | Edge.prototype.intersect = function (arr, cb) { 209 | arr = Array.isArray(arr) ? arr : [ arr ] 210 | arr = arr.map(function (gid) { return format(this.innerformat, String(gid)) }, this) 211 | arr.unshift(this.innerkey) 212 | db.sinter(arr, cb) 213 | } 214 | 215 | /** 216 | * Return a union (logical XOR) of inner edges with those of `arr` 217 | * 218 | * @param {String || Array} arr 219 | * @param {Function} cb 220 | */ 221 | 222 | Edge.prototype.union = function (arr, cb) { 223 | arr = Array.isArray(arr) ? arr : [ arr ] 224 | arr = arr.map(function (gid) { return format(this.innerformat, String(gid)) }, this) 225 | arr.unshift(this.innerkey) 226 | db.sunion(arr, cb) 227 | } 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-graph", 3 | "version": "0.2.3", 4 | "description": "a graph database using redis sets", 5 | "main": "index.js", 6 | "bin": {}, 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "redis" : "0.7.2", 13 | "tap": "~0.2.5" 14 | }, 15 | "scripts": { 16 | "test": "tap test/*.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/tblobaum/redis-graph.git" 21 | }, 22 | "homepage": "https://github.com/tblobaum/redis-graph", 23 | "keywords": [ 24 | "redis", 25 | "graph", 26 | "edge", 27 | "node" 28 | ], 29 | "author": { 30 | "name": "Thomas Blobaum", 31 | "email": "tblobaum@gmail.com", 32 | "url": "https://github.com/tblobaum/" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /test/groups-test.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var client = require('redis').createClient() 3 | , group = require('../'); 4 | 5 | group.setClient(client) 6 | 7 | var Tom = group('tom') 8 | , Bob = group('bob') 9 | , Bill = group('bill') 10 | , nodejs = group('nodejs') 11 | , otherGroup = group('otherGroup') 12 | , javascript = group('javascript') 13 | 14 | test('test all the things', function (t) { 15 | 16 | t.plan(100) 17 | 18 | Tom.membership.add('anonymous', function (error, result) { 19 | t.deepEqual(error, null, 'error should be null') 20 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 21 | }) 22 | 23 | Tom.membership.has('admin', function (error, result) { 24 | t.notOk(result, 'result should be falsy') 25 | }) 26 | 27 | Tom.membership.add('admin', function (error, result) { 28 | t.deepEqual(error, null, 'error should be null') 29 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 30 | }) 31 | 32 | Tom.membership.has('admin', function (error, result) { 33 | t.ok(result, 'result should be truthy') 34 | }); 35 | 36 | Tom.membership.add('one', function (error, result) { 37 | t.deepEqual(error, null, 'error should be null') 38 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 39 | }) 40 | 41 | Tom.membership.add('two', function (error, result) { 42 | t.deepEqual(error, null, 'error should be null') 43 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 44 | }) 45 | 46 | Bob.membership.add('anonymous', function (error, result) { 47 | t.deepEqual(error, null, 'error should be null') 48 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 49 | }) 50 | 51 | Bob.membership.add('one', function (error, result) { 52 | t.deepEqual(error, null, 'error should be null') 53 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 54 | }) 55 | 56 | Bob.membership.add('two', function (error, result) { 57 | t.deepEqual(error, null, 'error should be null') 58 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 59 | }) 60 | 61 | Bob.membership.add('three', function (error, result) { 62 | t.deepEqual(error, null, 'error should be null') 63 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 64 | }) 65 | 66 | Bob.membership.add('four', function (error, result) { 67 | t.deepEqual(error, null, 'error should be null') 68 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 69 | }) 70 | 71 | Bob.membership.add('five', function (error, result) { 72 | t.deepEqual(error, null, 'error should be null') 73 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 74 | }) 75 | 76 | Bill.membership.add('anonymous', function (error, result) { 77 | t.deepEqual(error, null, 'error should be null') 78 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 79 | }) 80 | 81 | Bill.membership.add('admin', function (error, result) { 82 | t.deepEqual(error, null, 'error should be null') 83 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 84 | }) 85 | 86 | Tom.membership.all(function (error, result) { 87 | t.deepEqual(error, null, 'error should be null') 88 | t.deepEqual(result.sort(), [ 'anonymous', 'admin', 'one', 'two' ].sort(), 'result should be the same') 89 | }) 90 | 91 | Bob.membership.all(function (error, result) { 92 | t.deepEqual(error, null, 'error should be null') 93 | t.deepEqual(result.sort(), [ 'anonymous', 'one', 'two', 'three', 'four', 'five' ].sort(), 'result should be the same') 94 | }) 95 | 96 | Bill.membership.all(function (error, result) { 97 | t.deepEqual(error, null, 'error should be null') 98 | t.deepEqual(result.sort(), [ 'anonymous', 'admin' ].sort(), 'result should be the same') 99 | }) 100 | 101 | Bill.membership.delete('admin', function (error, result) { 102 | t.deepEqual(error, null, 'error should be null') 103 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 104 | }) 105 | 106 | Tom.membership.intersect(Bob, function (error, result) { 107 | t.deepEqual(error, null, 'error should be null') 108 | t.deepEqual(result.sort(), [ 'anonymous', 'one', 'two' ].sort(), 'result should be the same') 109 | }) 110 | 111 | Tom.membership.union(Bob, function (error, result) { 112 | t.deepEqual(error, null, 'error should be null') 113 | t.deepEqual(result.sort(), [ 'anonymous', 'admin', 'one', 'two', 'three', 'four', 'five' ].sort(), 'result should be the same') 114 | }) 115 | 116 | Tom.membership.without(Bob, function (error, result) { 117 | t.deepEqual(error, null, 'error should be null') 118 | t.deepEqual(result.sort(), [ 'admin' ], 'result should be the same') 119 | }) 120 | 121 | Tom.membership.intersect([ Bob, Bill ], function (error, result) { 122 | t.deepEqual(error, null, 'error should be null') 123 | t.deepEqual(result.sort(), [ 'anonymous' ], 'result should be the same') 124 | }) 125 | 126 | Tom.membership.union([ Bob, Bill ], function (error, result) { 127 | t.deepEqual(error, null, 'error should be null') 128 | t.deepEqual(result.sort(), [ 'anonymous', 'admin', 'one', 'two', 'three', 'four', 'five' ].sort(), 'result should be the same') 129 | }) 130 | 131 | Tom.membership.without([ Bob, Bill ], function (error, result) { 132 | t.deepEqual(error, null, 'error should be null') 133 | t.deepEqual(result.sort(), [ 'admin' ], 'result should be the same') 134 | }) 135 | 136 | Tom.membership.add('nodejs', function (error, result) { 137 | t.deepEqual(error, null, 'error should be null') 138 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 139 | }) 140 | 141 | Tom.membership.add('otherGroup', function (error, result) { 142 | t.deepEqual(error, null, 'error should be null') 143 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 144 | }) 145 | 146 | Tom.membership.add('javascript', function (error, result) { 147 | t.deepEqual(error, null, 'error should be null') 148 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 149 | }) 150 | 151 | Bob.membership.add('javascript', function (error, result) { 152 | t.deepEqual(error, null, 'error should be null') 153 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 154 | }) 155 | 156 | Bill.membership.add('javascript', function (error, result) { 157 | t.deepEqual(error, null, 'error should be null') 158 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 159 | }) 160 | 161 | Tom.membership.all(function (error, result) { 162 | t.deepEqual(error, null, 'error should be null') 163 | t.deepEqual(result.sort(), [ 'javascript', 'nodejs', 'otherGroup', 'anonymous', 'admin', 'one', 'two' ].sort(), 'result should be the same') 164 | }) 165 | 166 | nodejs.members.has(javascript, function (error, result) { 167 | t.notOk(result, 'result should be falsy') 168 | }) 169 | 170 | nodejs.members.add(javascript, function (error, result) { 171 | t.deepEqual(error, null, 'error should be null') 172 | t.deepEqual(result, [ 1, 1 ], 'result should be the same') 173 | javascript.membership.add(nodejs, function (error, result) { 174 | t.deepEqual(error, null, 'error should be null') 175 | t.deepEqual(result, [ 0, 0 ], 'result should be the same') 176 | }) 177 | }) 178 | 179 | nodejs.members.has(javascript, function (error, result) { 180 | t.ok(result, 'result should be truthy') 181 | }) 182 | 183 | javascript.members.all(function (error, result) { 184 | t.deepEqual(error, null, 'error should be null') 185 | t.deepEqual(result.sort(), [ 'tom', 'bob', 'bill' ].sort(), 'result should be the same') 186 | }) 187 | 188 | nodejs.members.all(function (error, result) { 189 | t.deepEqual(error, null, 'error should be null') 190 | t.deepEqual(result.sort(), [ 'tom', 'javascript' ].sort(), 'result should be the same') 191 | }) 192 | 193 | Bill.membership.all(function (error, result) { 194 | t.deepEqual(error, null, 'error should be null') 195 | t.deepEqual(result.sort(), [ 'javascript', 'anonymous' ].sort(), 'result should be the same') 196 | }) 197 | 198 | Bill.members.all(function (error, result) { 199 | t.deepEqual(error, null, 'error should be null') 200 | t.deepEqual(result.sort(), [ ], 'result should be the same') 201 | }) 202 | 203 | otherGroup.members.all(function (error, result) { 204 | t.deepEqual(error, null, 'error should be null') 205 | t.deepEqual(result.sort(), [ 'tom' ], 'result should be the same') 206 | }) 207 | 208 | nodejs.members.members(function (error, result) { 209 | t.deepEqual(error, null, 'error should be null') 210 | t.deepEqual(result.sort(), [ 'bill', 'bob', 'tom' ].sort(), 'result should be the same') 211 | }) 212 | 213 | nodejs.members.membership(function (error, result) { 214 | t.deepEqual(error, null, 'error should be null') 215 | t.deepEqual(result.sort(), [ 'nodejs', 'javascript', 'otherGroup', 'anonymous', 'admin', 'two', 'one' ].sort(), 'result should be the same') 216 | }) 217 | 218 | nodejs.membership.membership(function (error, result) { 219 | t.deepEqual(error, null, 'error should be null') 220 | t.deepEqual(result.sort(), [ ], 'result should be the same') 221 | }) 222 | 223 | nodejs.membership.members(function (error, result) { 224 | t.deepEqual(error, null, 'error should be null') 225 | t.deepEqual(result.sort(), [ ], 'result should be the same') 226 | }) 227 | 228 | Tom.membership.intersect(nodejs, function (error, result) { 229 | t.deepEqual(error, null, 'error should be null') 230 | t.deepEqual(result.sort(), [ ], 'result should be the same') 231 | }) 232 | 233 | nodejs.members.intersect(javascript, function (error, result) { 234 | t.deepEqual(error, null, 'error should be null') 235 | t.deepEqual(result.sort(), [ 'tom' ], 'result should be the same') 236 | }) 237 | 238 | javascript.members.intersect(nodejs, function (error, result) { 239 | t.deepEqual(error, null, 'error should be null') 240 | t.deepEqual(result.sort(), [ 'tom' ], 'result should be the same') 241 | }) 242 | 243 | Tom.delete(function (error, result) { 244 | t.deepEqual(error, null, 'error should be null') 245 | t.type(result, Array, 'result should be the same') 246 | }) 247 | 248 | Bob.delete(function (error, result) { 249 | t.deepEqual(error, null, 'error should be null') 250 | t.type(result, Array, 'result should be the same') 251 | }) 252 | 253 | Bill.delete(function (error, result) { 254 | t.deepEqual(error, null, 'error should be null') 255 | t.type(result, Array, 'result should be the same') 256 | }) 257 | 258 | nodejs.delete(function (error, result) { 259 | t.deepEqual(error, null, 'error should be null') 260 | t.type(result, Array, 'result should be the same') 261 | }) 262 | 263 | otherGroup.delete(function (error, result) { 264 | t.deepEqual(error, null, 'error should be null') 265 | t.type(result, Array, 'result should be the same') 266 | }) 267 | 268 | javascript.delete(function (error, result) { 269 | t.deepEqual(error, null, 'error should be null') 270 | t.type(result, Array, 'result should be the same') 271 | }) 272 | 273 | }) 274 | 275 | test('close redis connection', function (t) { 276 | client.end() 277 | t.end() 278 | }) 279 | --------------------------------------------------------------------------------