├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── spec ├── support │ └── jasmine.json └── treeqlSpec.js └── treeql.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node, npm 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrei Gheorghe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TreeQL 2 | ====== 3 | 4 | JSON query and mutation library. It traverses a tree structure in post-order (leaves first, root last), across objects and arrays, returning the nodes which match the partial structure passed in the query, as well as allowing you to mutate or replace matched nodes. 5 | 6 | ## Usage 7 | 8 | ``` 9 | npm install treeql 10 | ``` 11 | 12 | ```javascript 13 | var treeql = require("treeql"); 14 | 15 | treeql.query(tree, query, function(node, variables) { 16 | // this will be called for every node matching the query 17 | // return replacement node [optional] 18 | }, function(resultTree, matchesCount) { 19 | // resultTree: the tree obtained after applying mutations 20 | // matchesCount: the number of nodes that matched the query 21 | }); 22 | ``` 23 | 24 | ## Examples 25 | 26 | ### Simple node selection 27 | 28 | ```javascript 29 | var data = { 30 | people: [{ 31 | name: "Andrei", 32 | age: 28 33 | }, { 34 | name: "Homer Simpson", 35 | age: 60 36 | }, { 37 | name: "Corina", 38 | age: 28 39 | }] 40 | } 41 | 42 | // Query for people aged 28 43 | treeql.query(data, { 44 | age: 28 45 | }, function(node) { 46 | // this callback will be called twice: 47 | // 1: node = { name: "Andrei", age: 28 } 48 | // 2: node = { name: "Corina", age: 28 } 49 | }); 50 | ``` 51 | 52 | ### Dynamic filter 53 | 54 | Instead of static values, you can use a function to filter based on more complex conditions. 55 | 56 | ```javascript 57 | var data = { 58 | people: [{ 59 | name: "Andrei", 60 | age: 28 61 | }, { 62 | name: "Homer Simpson", 63 | age: 60 64 | }, { 65 | name: "Corina", 66 | age: 28 67 | }] 68 | } 69 | 70 | // Query for people aged under 50 71 | treeql.query(data, { 72 | age: x => x < 50 73 | }, function(node) { 74 | // this callback will be called twice: 75 | // 1: node = { name: "Andrei", age: 28 } 76 | // 2: node = { name: "Corina", age: 28 } 77 | }); 78 | ``` 79 | 80 | ### Variables 81 | 82 | The library supports variable definition in the query to ensure it matches the same value across different parts of the `query` object. 83 | 84 | ```javascript 85 | var $ = treeql.variable; 86 | 87 | var tree = { 88 | person: { 89 | "name": "Andrei", 90 | "age": 28, 91 | "os": "MacOS", 92 | "team": { 93 | "name": "idevelop", 94 | "preferred_os": "MacOS" 95 | } 96 | } 97 | }; 98 | 99 | // Query for people using their team's preferred OS 100 | var query = { 101 | "os": $("operating_system"), 102 | "team": { 103 | "preferred_os": $("operating_system") 104 | } 105 | }; 106 | 107 | treeql.query(tree, query, function(node, variables) { 108 | // node = { "name": "Andrei", ... } 109 | // variables = { "operating_system": "MacOS" } 110 | }); 111 | ``` 112 | 113 | ### Tree mutation 114 | 115 | TreeQL also supports mutating the set of matched nodes. Any mutation happens on a copy of the original structure, which remains unchanged. To obtain the resulting mutated tree, the `query` function takes on optional second callback parameter, `completeCallback`, which gets invoked at the end and receives the arguments `resultTree` and `matchesCount`. 116 | 117 | To mutate the tree you can either change properties of a matched node inside the node callback: 118 | 119 | ```javascript 120 | treeql.query(tree, { 121 | "name": "Andrei" 122 | }, function(node) { 123 | node.age++; 124 | }, function(resultTree, matchesCount) { 125 | // resultTree contains the original tree, with all the age values incremented 126 | }); 127 | ``` 128 | 129 | Alternatively, if the node callback returns a value, it will replace the matched node entirely. 130 | 131 | ```javascript 132 | var data = [{ 133 | name: "Andrei" 134 | }, { 135 | differentProperty: 10 136 | }] 137 | 138 | // Match objects with "name" property, regardless of value 139 | var query = { 140 | name: undefined 141 | }; 142 | 143 | treeql.query(data, query, function(node) { 144 | return { 145 | name: node.name.toLowerCase() 146 | } 147 | }); 148 | ``` 149 | 150 | Here's how you can sum up a nested tree of number arrays. 151 | 152 | ```javascript 153 | var data = [1, 2, 3, [4, 5, [6]]]; 154 | 155 | var query = []; // Match any array 156 | 157 | treeql.query(data, query, function(array) { 158 | var sum = 0; 159 | array.map(function(value) { 160 | sum += value; 161 | }); 162 | 163 | return sum; 164 | }, function(result) { 165 | // result = 21 166 | }); 167 | ``` 168 | 169 | You can find a couple more examples in the `treeSpec.js` unit test suite. 170 | 171 | ## Author 172 | 173 | **Andrei Gheorghe** 174 | 175 | * [About me](http://idevelop.github.com) 176 | * LinkedIn: [linkedin.com/in/idevelop](http://www.linkedin.com/in/idevelop) 177 | * Twitter: [@idevelop](http://twitter.com/idevelop) 178 | 179 | ## License 180 | 181 | - TreeQL is licensed under the MIT License. 182 | - I am providing code in this repository to you under an open source license. Because this is my personal repository, the license you receive to my code is from me and not from my employer (Facebook). 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treeql", 3 | "version": "1.0.1", 4 | "description": "JSON query and mutation library. It traverses a tree structure in post-order (leaves first, root last), across objects and arrays, returning the nodes which match the partial structure passed in the query, as well as allowing you to mutate or replace matched nodes.", 5 | "homepage": "https://github.com/idevelop/treeql", 6 | "main": "treeql.js", 7 | "author": { 8 | "name": "Andrei Gheorghe", 9 | "email": "andreig@gmail.com", 10 | "url": "http://idevelop.github.com/" 11 | }, 12 | "files": [ 13 | "LICENSE", 14 | "README.md", 15 | "treeql.js", 16 | "spec", 17 | "package.json" 18 | ], 19 | "licenses": [{ 20 | "type": "MIT", 21 | "url": "http://github.com/idevelop/treeql/raw/master/LICENSE" 22 | }], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/idevelop/treeql.git" 26 | }, 27 | "devDependencies" : { 28 | "jasmine": "latest" 29 | }, 30 | "scripts": { 31 | "test": "jasmine" 32 | } 33 | } -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /spec/treeqlSpec.js: -------------------------------------------------------------------------------- 1 | var treeql = require("../treeql"); 2 | var $ = treeql.variable; 3 | 4 | describe("selecting", function() { 5 | it("should match root", function() { 6 | var tree = { 7 | "name": "Andrei", 8 | "age": 28 9 | }; 10 | 11 | var query = { 12 | "name": "Andrei" 13 | }; 14 | 15 | treeql.query(tree, query, function(node) { 16 | expect(node.age).toEqual(28); 17 | }, function(resultTree, matchesCount) { 18 | expect(matchesCount).toEqual(1); 19 | }); 20 | }); 21 | 22 | it("should match simple type", function() { 23 | treeql.query("Andrei", "Andrei", null, function(resultTree, matchesCount) { 24 | expect(resultTree).toEqual("Andrei"); 25 | expect(matchesCount).toEqual(1); 26 | }); 27 | }); 28 | 29 | it("should not match array with object", function() { 30 | var tree = { 31 | name: "Andrei" 32 | }; 33 | 34 | var query = []; 35 | 36 | treeql.query(tree, query, null, function(resultTree, matchesCount) { 37 | expect(matchesCount).toEqual(0); 38 | }); 39 | }); 40 | 41 | it("should match undefined value", function() { 42 | var data = [{ 43 | name: "Andrei" 44 | }, { 45 | differentProperty: 10 46 | }]; 47 | 48 | // Match objects with "name" property 49 | var query = { 50 | name: undefined 51 | }; 52 | 53 | treeql.query(data, query, null, function(resultTree, matchesCount) { 54 | expect(matchesCount).toEqual(1); 55 | }); 56 | }); 57 | 58 | it("should find objects based on function", function() { 59 | var data = [{ 60 | name: "Andrei", 61 | age: 28 62 | }, { 63 | name: "Corina", 64 | age: 26 65 | }, { 66 | name: "Homer Simpson", 67 | age: 60 68 | }]; 69 | 70 | treeql.query(data, { 71 | age: function(age) { return age < 50; } 72 | }, null, function(resultTree, matchesCount) { 73 | expect(matchesCount).toEqual(2); 74 | }); 75 | }); 76 | 77 | it("should match partial array definition with index > 0", function() { 78 | var data = { 79 | properties: [{ 80 | type: 'a', 81 | value: 4 82 | }, { 83 | type: 'b', 84 | value: 5 85 | }] 86 | }; 87 | 88 | treeql.query(data, { 89 | properties: [{ 90 | value: function(v) { return v == 5; } 91 | }] 92 | }, null, function(result, count) { 93 | expect(count).toEqual(1); 94 | }); 95 | }); 96 | 97 | it("should find simple object", function() { 98 | var tree = { 99 | person: { 100 | "name": "Andrei", 101 | "age": 28 102 | } 103 | }; 104 | 105 | var query = { 106 | "name": "Andrei" 107 | }; 108 | 109 | treeql.query(tree, query, function(node) { 110 | expect(node.age).toEqual(28); 111 | }, function(resultTree, matchesCount) { 112 | expect(matchesCount).toEqual(1); 113 | }); 114 | }); 115 | 116 | it("should find variables that appear once", function() { 117 | var tree = { 118 | person: { 119 | "name": "Andrei", 120 | "age": 28, 121 | "city": "London" 122 | } 123 | }; 124 | 125 | var query = { 126 | "name": "Andrei", 127 | "age": $("age"), 128 | "city": $("city") 129 | }; 130 | 131 | treeql.query(tree, query, function(node, variables) { 132 | expect(node.age).toEqual(28); 133 | expect(variables.age).toEqual(28); 134 | expect(variables.city).toEqual("London"); 135 | }, function(resultTree, matchesCount) { 136 | expect(matchesCount).toEqual(1); 137 | }); 138 | }); 139 | 140 | it("should find identical variables that appear multiple times", function() { 141 | var tree = { 142 | person: { 143 | "name": "Andrei", 144 | "age": 28, 145 | "os": "MacOS", 146 | "team": { 147 | "name": "idevelop", 148 | "favorite_os": "MacOS" 149 | } 150 | } 151 | }; 152 | 153 | var query = { 154 | "os": $("operating_system"), 155 | "team": { 156 | "favorite_os": $("operating_system") 157 | } 158 | }; 159 | 160 | treeql.query(tree, query, function(node, variables) { 161 | expect(node.name).toEqual("Andrei"); 162 | expect(variables.operating_system).toEqual("MacOS"); 163 | }, function(resultTree, matchesCount) { 164 | expect(matchesCount).toEqual(1); 165 | }); 166 | }); 167 | 168 | it("should fail to match same variable with different values", function() { 169 | var tree = { 170 | person: { 171 | "name": "Andrei", 172 | "age": 28, 173 | "os": "MacOS", 174 | "team": { 175 | "name": "idevelop", 176 | "favorite_os": "Windows" 177 | } 178 | } 179 | }; 180 | 181 | var query = { 182 | "os": $("operating_system"), 183 | "team": { 184 | "favorite_os": $("operating_system") 185 | } 186 | }; 187 | 188 | treeql.query(tree, query, null, function(resultTree, matchesCount) { 189 | expect(matchesCount).toEqual(0); 190 | }); 191 | }); 192 | }); 193 | 194 | describe("replacing", function() { 195 | it("should update a node value", function() { 196 | var tree = [{ 197 | name: "Andrei", 198 | age: 28 199 | }, { 200 | name: "Corina", 201 | age: 26 202 | }]; 203 | 204 | treeql.query(tree, { 205 | "name": "Andrei" 206 | }, function(node) { 207 | node.age++; 208 | }, function(resultTree, matchesCount) { 209 | expect(resultTree.length).toEqual(2); 210 | 211 | treeql.query(resultTree, { 212 | "name": "Andrei" 213 | }, function(node) { 214 | expect(node.age).toEqual(29); 215 | }); 216 | }); 217 | }); 218 | 219 | it("should replace simple value", function() { 220 | treeql.query({ 221 | name: "Andrei" 222 | }, "Andrei", function() { 223 | return "Homer"; 224 | }, function(result) { 225 | expect(result.name).toEqual("Homer"); 226 | }); 227 | }); 228 | 229 | it("should replace simple node", function() { 230 | var tree = [{ 231 | name: "Andrei", 232 | age: 28 233 | }, { 234 | name: "Corina", 235 | age: 26 236 | }]; 237 | 238 | treeql.query(tree, { 239 | "name": "Andrei" 240 | }, function(node) { 241 | return { 242 | name: "Andrei Gheorghe", 243 | age: 28.5 244 | }; 245 | }, function(resultTree, matchesCount) { 246 | expect(resultTree.length).toEqual(2); 247 | 248 | treeql.query(resultTree, { 249 | "name": "Andrei Gheorghe" 250 | }, null, function(resultTree, matchesCount) { 251 | expect(matchesCount).toEqual(1); 252 | }); 253 | }); 254 | }); 255 | 256 | it("should recursively sum up a tree of numbers", function() { 257 | var tree = [1, 2, 3, [4, 5, [6]]]; 258 | var query = []; 259 | 260 | treeql.query(tree, query, function(node) { 261 | var sum = node.reduce(function(value, total) { 262 | return value + total; 263 | }, 0); 264 | 265 | return sum; 266 | }, function(resultTree, matchesCount) { 267 | expect(resultTree).toEqual(21); 268 | }); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /treeql.js: -------------------------------------------------------------------------------- 1 | var treeQLVariableKey = "__treeql_variable__"; 2 | 3 | function isSimpleType(variable) { 4 | return ["number", "boolean", "string"].indexOf(typeof variable) > -1; 5 | } 6 | 7 | function isVariableNode(node) { 8 | return (typeof node === "object") && node.hasOwnProperty(treeQLVariableKey); 9 | } 10 | 11 | function treeMatch(tree, query, variables) { 12 | if (query === undefined) { 13 | return true; 14 | } 15 | 16 | if (typeof query === "function") { 17 | try { 18 | return query(tree); 19 | } catch(e) { 20 | return false; 21 | } 22 | } 23 | 24 | if (isVariableNode(query)) { 25 | var variableName = query[treeQLVariableKey]; 26 | if (variables.hasOwnProperty(variableName)) { 27 | return treeMatch(tree, variables[variableName], variables); 28 | } else { 29 | variables[variableName] = tree; 30 | return true; 31 | } 32 | } 33 | 34 | if (typeof query !== typeof tree) { 35 | return false; 36 | } 37 | 38 | if (Array.isArray(query) !== Array.isArray(tree)) { 39 | return false; 40 | } 41 | 42 | if (isSimpleType(query)) { 43 | return tree === query; 44 | } 45 | 46 | if (query === null || tree === null) { 47 | return tree === query; 48 | } 49 | 50 | if (Array.isArray(query)) { 51 | // Comparing two arrays, each element in the query array must match at least one element in the tree array 52 | for (var i = 0; i < query.length; i++) { 53 | var foundMatch = false; 54 | for (var j = 0; j < tree.length; j++) { 55 | if (treeMatch(tree[j], query[i], variables)) { 56 | foundMatch = true; 57 | break; 58 | } 59 | } 60 | 61 | if (!foundMatch) { 62 | return false; 63 | } 64 | } 65 | } else { 66 | // Comparing two objects, all query keys should be included in object keys 67 | for (var key in query) { 68 | if (!tree.hasOwnProperty(key)) { 69 | return false; 70 | } else { 71 | if (!treeMatch(tree[key], query[key], variables)) { 72 | return false; 73 | } 74 | } 75 | } 76 | } 77 | 78 | return true; 79 | } 80 | 81 | function findMatches(tree, query, callback) { 82 | var matches = 0; 83 | 84 | if (typeof tree === "object") 85 | for (var key in tree) { 86 | if (tree.hasOwnProperty(key)) { 87 | var result = findMatches(tree[key], query, callback); 88 | matches += result.matches; 89 | tree[key] = result.tree; 90 | } 91 | } 92 | 93 | var variables = {}; 94 | if (treeMatch(tree, query, variables)) { 95 | matches++; 96 | 97 | var replacement = callback(tree, variables); 98 | if (replacement !== undefined) { 99 | return { 100 | matches: matches, 101 | tree: replacement 102 | }; 103 | } 104 | } 105 | 106 | return { 107 | matches: matches, 108 | tree: tree 109 | }; 110 | } 111 | 112 | exports.query = function(tree, query, nodeCallback, completeCallback) { 113 | nodeCallback = nodeCallback || function(){}; 114 | completeCallback = completeCallback || function(){}; 115 | 116 | var treeCopy = JSON.parse(JSON.stringify(tree)); 117 | 118 | var result = findMatches(treeCopy, query, nodeCallback); 119 | 120 | completeCallback(result.tree, result.matches); 121 | }; 122 | 123 | exports.variable = function(name) { 124 | var node = {}; 125 | node[treeQLVariableKey] = name; 126 | return node; 127 | }; 128 | --------------------------------------------------------------------------------