├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── README.md ├── key-tree-store.js ├── package.json └── test └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style = space 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.{js,html,jade,md}] 10 | indent_size = 4 11 | 12 | [*.json] 13 | indent_size = 2 14 | 15 | [*.js] 16 | jslint_happy = true 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.10' 5 | - '0.12' 6 | - 'iojs' 7 | matrix: 8 | fast_finish: true 9 | notifications: 10 | email: 11 | on_success: never 12 | on_failure: always 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # key-tree-store 2 | 3 | Simple tool for storing/retrieving objects events based hierarchical keypaths. 4 | 5 | It lets you store and retrive objects that are at an equal or deeper key path than what you give it. 6 | 7 | ## install 8 | 9 | ``` 10 | npm install key-tree-store 11 | ``` 12 | 13 | ## example 14 | 15 | Assume you've got a structure like this: 16 | 17 | ```js 18 | { 19 | 'first': [ {obj: 1}, {obj: 2} ], 20 | 'first.stuff': [ {obj: 3} ], 21 | 'first.something.other': [ {obj: 4}, {obj: 5} ] 22 | } 23 | ``` 24 | 25 | Then you can retrive it by key. Where it returns anything at or deeper than level supplied. 26 | 27 | ```javascript 28 | var KeyTree = require('key-tree-store'); 29 | 30 | var tree = new KeyTree(); 31 | 32 | tree.add('first', {id: 'one'}); 33 | tree.add('first.second', {id: 'two'}); 34 | tree.add('first.second', {id: 'three'}); 35 | tree.add('first.second.third', {id: 'four'}); 36 | 37 | // now we can retrieve them by key 38 | tree.get('first'); // returns all of them 39 | tree.get('first.second'); // returns array of objects two, three and four 40 | tree.get('first.second.third'); // returns array of object four; 41 | 42 | // the `get` method returns them all in an array 43 | // if we still need them grouped by key we can use 44 | // `getGrouped` 45 | tree.getGrouped('first.second'); // returns {'first.second': [...], 'first.second.third': [...]} 46 | 47 | // calling `.get()` or `.getGrouped` without arguments 48 | // returns all in appropriate format 49 | 50 | // that's all there is to it 51 | 52 | ``` 53 | 54 | removing items: 55 | 56 | ```javascript 57 | var KeyTree = require('key-tree-store'); 58 | 59 | var tree = new KeyTree(); 60 | var obj1 = {obj: '1'}; 61 | 62 | tree.add('key.path', obj1); 63 | 64 | // removes it no matter what key 65 | tree.remove(obj1); 66 | ``` 67 | 68 | running items: 69 | 70 | ```javascript 71 | // as a shortcut, there's also the `run` method 72 | // to help you run functions that match they keypath 73 | // this assumes you're storing functions, of course. 74 | var KeyTree = require('key-tree-store'); 75 | 76 | var tree = new KeyTree(); 77 | 78 | tree.add('key.path', function () { 79 | console.log('function ran!'); 80 | }); 81 | 82 | tree.run('key'); //=> function ran! 83 | 84 | // you can also optionally pass a context or arguments to run them with 85 | tree.run('key', {some: 'object'}); 86 | 87 | // with arguments 88 | tree.run('key', {some: 'object'}, 'arg1', 'arg2'); 89 | 90 | ``` 91 | 92 | ## credits 93 | 94 | If you like this follow [@HenrikJoreteg](http://twitter.com/henrikjoreteg) on twitter. 95 | 96 | ## license 97 | 98 | MIT 99 | 100 | -------------------------------------------------------------------------------- /key-tree-store.js: -------------------------------------------------------------------------------- 1 | var slice = Array.prototype.slice; 2 | 3 | // our constructor 4 | function KeyTreeStore(options) { 5 | options = options || {}; 6 | if (typeof options !== 'object') { 7 | throw new TypeError('Options must be an object'); 8 | } 9 | var DEFAULT_SEPARATOR = '.'; 10 | 11 | this.storage = {}; 12 | this.separator = options.separator || DEFAULT_SEPARATOR; 13 | } 14 | 15 | // add an object to the store 16 | KeyTreeStore.prototype.add = function (keypath, obj) { 17 | var arr = this.storage[keypath] || (this.storage[keypath] = []); 18 | arr.push(obj); 19 | }; 20 | 21 | // remove an object 22 | KeyTreeStore.prototype.remove = function (obj) { 23 | var path, arr; 24 | for (path in this.storage) { 25 | arr = this.storage[path]; 26 | arr.some(function (item, index) { 27 | if (item === obj) { 28 | arr.splice(index, 1); 29 | return true; 30 | } 31 | }); 32 | } 33 | }; 34 | 35 | // get array of all all relevant functions, without keys 36 | KeyTreeStore.prototype.get = function (keypath) { 37 | var res = []; 38 | var key; 39 | 40 | for (key in this.storage) { 41 | if (!keypath || keypath === key || key.indexOf(keypath + this.separator) === 0) { 42 | res = res.concat(this.storage[key]); 43 | } 44 | } 45 | 46 | return res; 47 | }; 48 | 49 | // get all results that match keypath but still grouped by key 50 | KeyTreeStore.prototype.getGrouped = function (keypath) { 51 | var res = {}; 52 | var key; 53 | 54 | for (key in this.storage) { 55 | if (!keypath || keypath === key || key.indexOf(keypath + this.separator) === 0) { 56 | res[key] = slice.call(this.storage[key]); 57 | } 58 | } 59 | 60 | return res; 61 | }; 62 | 63 | // get all results that match keypath but still grouped by key 64 | KeyTreeStore.prototype.getAll = function (keypath) { 65 | var res = {}; 66 | var key; 67 | 68 | for (key in this.storage) { 69 | if (keypath === key || key.indexOf(keypath + this.separator) === 0) { 70 | res[key] = slice.call(this.storage[key]); 71 | } 72 | } 73 | 74 | return res; 75 | }; 76 | 77 | // run all matches with optional context 78 | KeyTreeStore.prototype.run = function (keypath, context) { 79 | var args = slice.call(arguments, 2); 80 | this.get(keypath).forEach(function (fn) { 81 | fn.apply(context || this, args); 82 | }); 83 | }; 84 | 85 | module.exports = KeyTreeStore; 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "key-tree-store", 3 | "description": "Simple tool for storing/retrieving objects events based hierarchical keypaths.", 4 | "version": "1.3.0", 5 | "author": "Henrik Joreteg ", 6 | "bugs": { 7 | "url": "https://github.com/HenrikJoreteg/key-tree-store/issues" 8 | }, 9 | "devDependencies": { 10 | "tape": "^4.0.3" 11 | }, 12 | "directories": { 13 | "test": "test" 14 | }, 15 | "files": [ 16 | "key-tree-store.js" 17 | ], 18 | "homepage": "https://github.com/HenrikJoreteg/key-tree-store", 19 | "keywords": [ 20 | "events", 21 | "keypath", 22 | "trigger" 23 | ], 24 | "license": "MIT", 25 | "main": "key-tree-store", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/HenrikJoreteg/key-tree-store.git" 29 | }, 30 | "scripts": { 31 | "test": "node test/*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var KeyTree = require('../key-tree-store'); 3 | 4 | test('throw if supplied options is not an object', function (t) { 5 | t.throws(function() { 6 | new KeyTree('|'); 7 | }, TypeError); 8 | t.end(); 9 | }); 10 | 11 | test('check that an object is passed as options, or non at all', function (t) { 12 | t.doesNotThrow(function() { 13 | new KeyTree(); 14 | }); 15 | t.doesNotThrow(function() { 16 | new KeyTree({}); 17 | }); 18 | t.end(); 19 | }); 20 | 21 | test('`add` should store objects', function (t) { 22 | var tree = new KeyTree(); 23 | var one = {id: 'one'}; 24 | var two = {id: 'two'}; 25 | var three = {id: 'three'}; 26 | var four = {id: 'four'}; 27 | 28 | tree.add('first', one); 29 | tree.add('first.second', two); 30 | tree.add('first.second', three); 31 | tree.add('first.second.third', four); 32 | 33 | t.equal(Object.keys(tree.storage).length, 3); 34 | 35 | t.equal(tree.storage['first.second'].length, 2, 'should be two for `first.second` key'); 36 | t.equal(tree.get().length, 4, 'should return all'); 37 | t.equal(tree.get('first').length, 4, 'should be 4 that match'); 38 | t.equal(tree.get('first.second').length, 3, 'should be 3 that match'); 39 | t.equal(tree.get('first.second.third').length, 1, 'should be 1 that match'); 40 | 41 | t.equal(tree.get('second.third').length, 0, 'keypaths should start at the start'); 42 | 43 | t.equal(tree.get('first.seco').length, 0, 'keypaths must be the full path, or end in a . to match'); 44 | 45 | t.deepEqual(tree.getGrouped('first.second'), { 46 | 'first.second': [two, three], 47 | 'first.second.third': [four] 48 | }); 49 | 50 | t.deepEqual(tree.getGrouped(), { 51 | 'first': [one], 52 | 'first.second': [two, three], 53 | 'first.second.third': [four] 54 | }); 55 | 56 | tree.remove(two); 57 | t.equal(tree.get('first').length, 3, 'should be 3 that match after removal'); 58 | 59 | t.end(); 60 | }); 61 | 62 | test('a different `separator` should be respected', function (t) { 63 | var tree = new KeyTree({ separator: '|' }); 64 | var one = {id: 'one'}; 65 | var two = {id: 'two'}; 66 | var three = {id: 'three'}; 67 | var four = {id: 'four'}; 68 | 69 | tree.add('first', one); 70 | tree.add('first|second', two); 71 | tree.add('first|second', three); 72 | tree.add('first|second|third', four); 73 | 74 | t.equal(Object.keys(tree.storage).length, 3); 75 | 76 | t.equal(tree.storage['first|second'].length, 2, 'should be two for `first|second` key'); 77 | t.equal(tree.get().length, 4, 'should return all'); 78 | t.equal(tree.get('first').length, 4, 'should be 4 that match'); 79 | t.equal(tree.get('first|second').length, 3, 'should be 3 that match'); 80 | t.equal(tree.get('first|second|third').length, 1, 'should be 1 that match'); 81 | 82 | t.equal(tree.get('second|third').length, 0, 'keypaths should start at the start'); 83 | 84 | t.equal(tree.get('first|seco').length, 0, 'keypaths must be the full path'); 85 | 86 | t.deepEqual(tree.getGrouped('first|second'), { 87 | 'first|second': [two, three], 88 | 'first|second|third': [four] 89 | }); 90 | 91 | t.deepEqual(tree.getGrouped(), { 92 | 'first': [one], 93 | 'first|second': [two, three], 94 | 'first|second|third': [four] 95 | }); 96 | 97 | tree.remove(two); 98 | t.equal(tree.get('first').length, 3, 'should be 3 that match after removal'); 99 | 100 | t.end(); 101 | }); 102 | 103 | test('`run`', function (t) { 104 | var tree = new KeyTree(); 105 | var oneRan, twoRan; 106 | var oneContext, twoContext; 107 | var oneArgs; 108 | var one = function () { 109 | oneRan = true; 110 | oneContext = this; 111 | oneArgs = arguments; 112 | }; 113 | var two = function () { 114 | twoRan = true; 115 | twoContext = this; 116 | }; 117 | 118 | function reset() { 119 | oneRan = false; 120 | twoRan = false; 121 | oneContext = null; 122 | twoContext = null; 123 | } 124 | 125 | reset(); 126 | 127 | tree.add('one', one); 128 | tree.add('two', two); 129 | 130 | tree.run(); 131 | 132 | t.ok(oneRan); 133 | t.ok(twoRan); 134 | 135 | reset(); 136 | 137 | tree.run('one'); 138 | t.ok(oneRan); 139 | t.notOk(twoRan); 140 | 141 | reset(); 142 | 143 | var context = {}; 144 | tree.run('one', context, 'hello', 'world'); 145 | t.equal(oneContext, context); 146 | t.equal(oneArgs[0], 'hello'); 147 | t.equal(oneArgs[1], 'world'); 148 | 149 | t.end(); 150 | }); 151 | --------------------------------------------------------------------------------