├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── keys.js ├── make.js ├── package.json ├── simple ├── maps.js └── reduces.js ├── test ├── filter-ssb.js ├── filter.js ├── flexible.js ├── groups.js ├── keys.js ├── map.js ├── project.js ├── reduce-stream.js ├── reduce.js └── util.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Dominic Tarr 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be 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 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # map-filter-reduce 2 | 3 | A functional query engine, that operates over streams of js objects, 4 | and can be optimized via database indexes. 5 | 6 | ## example 7 | 8 | count the total number of mentions in ssb. 9 | 10 | ``` js 11 | var mfr = require('map-filter-reduce') 12 | pull( 13 | ssbc.createLogStream(), 14 | mfr([ 15 | //get all posts. 16 | {$filter: { value: { content: { type: 'post' } } }}, 17 | 18 | //extract the mentions 19 | {$map: {mentions: ['value', 'content', 'mentions', 'length']}}, 20 | 21 | //sum all the lengths 22 | {$reduce: {$sum: true}} 23 | ]) 24 | pull.log() 25 | ) 26 | ``` 27 | 28 | This example uses a full scan, so it looks at every object in the 29 | database. But if we have indexes, that can be made much more efficient, 30 | see [flumeview-query](https://github.com/flumedb/flumeview-query) 31 | 32 | ## processing streams 33 | 34 | `map-filter-reduce` provides three functional stream processing 35 | operators. usually, generally, filter should be used first, 36 | then map then reduce, but operators can be combined in any order, 37 | and used any number of times. 38 | 39 | ### filter 40 | 41 | a filter removes items from an incoming stream. 42 | 43 | #### basic values: strings, numbers, booleans 44 | 45 | strings, numbers and booleans match exactly. 46 | 47 | ``` js 48 | {$filter: 'okay'} 49 | ``` 50 | matches all strings that equal `"okay"` 51 | 52 | #### $prefix 53 | 54 | `$prefix` operator matches strings that start with a given string. 55 | ``` js 56 | {$filter: {$prefix: 'o'}} 57 | ``` 58 | this will match `"okay"` `"ok"` `"oh no"`, etc. 59 | 60 | #### $gt, $lt, $gte, $lte 61 | 62 | match values that are greater than, less than, greater than or equal, or less than or equal. 63 | A greater operator and a lesser operator may be combined into one comparison. 64 | 65 | ``` js 66 | {$filter: {$gte: 0, $lt: 1}} 67 | ``` 68 | will filter numbers from 0 up to but not including 1. 69 | 70 | #### $eq, $ne, $not, $truthy, $is, $in, $type 71 | 72 | basic operations: 73 | 74 | * `{$eq: value}` test input is equal to value 75 | * `{$ne: value}` test input is not equal to value 76 | * `{$truthy: true}` test if input converts to true. 77 | * `{$not: true}` test if input converts to false. 78 | * `{$type: true}` returns the `typeof` the input. (useful in $map, but not $filter) 79 | * `{$is: 'object'|'string'|'number'|'boolean'|'undefined'}` test if input is particular type, use instead of `$type` in filters. 80 | * `{$in: [...]}` check if input is one of a set. input must be a primitive, not an object. 81 | 82 | #### objects 83 | 84 | an object is used to filter objects that have the same properties. 85 | an object is combined with other operators that apply to the keys. 86 | 87 | ``` js 88 | {$filter: {name: {$prefix: 'bob'}, age: {$gt: 10}}} 89 | ``` 90 | select "bob", "bobby", and "bobalobadingdog", if their age is greater than 10. 91 | 92 | #### arrays 93 | 94 | arrays are used treated somewhat like strings, in that you can also 95 | use the `$prefix` and `$gt, $lt, $gte, $lte` operators. 96 | 97 | 98 | arrays are like objects, but their keys are compared in order from left to right. 99 | 100 | {$filter: {$prefix: ['okay']}} 101 | 102 | ### map 103 | 104 | If the incoming stream has fields you are not interested in, 105 | or fields that you'd like to remap, you can _map_ those objects. 106 | 107 | lets say, we have a nested object incoming, but we just want to pluck off 108 | the a nested name field. 109 | 110 | ``` js 111 | {$map: {name: ['value', 'name']}} 112 | ``` 113 | 114 | if the input was `{key: k, value: {foo: blah, name: 'bob'}}` 115 | the output would just be `{name: 'bob'}`. 116 | 117 | #### key: strings, numbers 118 | 119 | strings and numbers are used to get keys off the input. 120 | 121 | ``` js 122 | {$map: 'value'} 123 | ``` 124 | would return the value property of the input, and drop the rest. 125 | use numbers (integers) if the input will be an array. 126 | 127 | #### keys: objects 128 | 129 | for a {} object, each value is considered a rule, and applied to 130 | the value of the incoming data. a value with the transformations applied 131 | will be returned. 132 | 133 | the keys in the object do not need to be the same as in the input, 134 | since the values will map to which keys to operate on from the input. 135 | 136 | ``` js 137 | {$map: {foo: 'bar', bar: 'foo'}} 138 | ``` 139 | transform the input stream into a stream with `foo` and `bar` keys switched, 140 | and any other keys dropped. 141 | 142 | #### path: arrays 143 | 144 | arrays are used to lookup deep into an object. they are analogous to useing 145 | chaining dots in javascript. 146 | 147 | ``` js 148 | {$map: ['value', 'name']} 149 | ``` 150 | get the value property, and then get the name property off that. 151 | 152 | #### wildcard 153 | 154 | to get _everything_ use `true` 155 | 156 | ``` js 157 | {$map: true} 158 | ``` 159 | say, we have an object with _many_ keys, and we want to reduce that to just 160 | `timestamp` and then put everything under `value` 161 | 162 | ``` js 163 | {$map: {timestamp: 'timestamp', value: true}} 164 | ``` 165 | 166 | ### reduce 167 | 168 | reduce is used to aggregate many values into a representative value. 169 | `count`, `sum`, `min`, `max`, are all reduces. 170 | 171 | #### $count 172 | 173 | count how many items are in the input 174 | ``` js 175 | {$reduce: {$count: true}} 176 | ``` 177 | 178 | #### $sum 179 | 180 | sum elements in the input. input elements must be numbers. 181 | sums can be applied to paths, or strings to get the values to sum. 182 | 183 | add up all the values for `value.age` 184 | ``` js 185 | {$reduce: {$sum: ['value', 'age']}} 186 | ``` 187 | 188 | #### $min, $max 189 | 190 | get the minimum or maximum for a value. 191 | 192 | ``` js 193 | {$reduce: {$min: ['value', 'age']}} 194 | ``` 195 | 196 | #### $collect 197 | 198 | get all values an put them into an array. 199 | 200 | get all values for `foo` 201 | ``` js 202 | {$reduce: {$collect: 'foo'}} 203 | ``` 204 | this may produce a large value if there are many items in the input stream. 205 | 206 | #### objects 207 | 208 | to apply multiple reduce functions, put them in an object with 209 | key names. 210 | 211 | ``` js 212 | {$reduce: { 213 | youngest: {$min: ['value','age']}, 214 | oldest: {$max: ['value', 'age']} 215 | }} 216 | ``` 217 | #### groups 218 | 219 | To group, use the other reduce functions 220 | along side path expressions, as used under `$map` 221 | 222 | In the following query, 223 | ``` js 224 | {$reduce: { 225 | country: 'country', 226 | population: {$count: true} 227 | }} 228 | ``` 229 | count items per country. Will give a stream of items each with 230 | a `{country, population}` fields. 231 | 232 | This would return a object, with the shape `{:{: }}`. 233 | 234 | Note that, like in `$map` arrays can be used to drill deep into 235 | an object. 236 | ``` js 237 | {$reduce: {name: ['value', 'name'], posts: {$count: true}}} 238 | ``` 239 | 240 | TODO: group by time ranges (day, month, week, year, etc) 241 | 242 | ### $sort 243 | 244 | you can also sort the output of a some paths: 245 | 246 | ``` js 247 | {$sort: [['foo', 'bar'], ['baz']]} 248 | ``` 249 | 250 | the output is ordered by the value at the path `foo.bar`, then if two items are equal 251 | at that value, then they are ordered by `baz`. 252 | 253 | ## License 254 | 255 | MIT 256 | 257 | 258 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream/pull') 2 | var pullFilter = require('pull-stream/throughs/filter') 3 | var pullFlatten = require('pull-stream/throughs/flatten') 4 | var pullMap = require('pull-stream/throughs/map') 5 | var pullReduce = require('pull-stream/sinks/reduce') 6 | var pullSort = require('pull-sort') 7 | var CompareAt = require('compare-at-paths') 8 | 9 | var make = require('./make') 10 | var SinkThrough = require('pull-sink-through') 11 | 12 | function first (q) { 13 | for(var k in q) return k 14 | } 15 | 16 | function get (q) { 17 | var k = first(q) 18 | var s = k.substring(1) 19 | if(k[0] == '$' && exports[s]) return exports[s](q) 20 | throw new Error('unknown function:'+ k) 21 | } 22 | 23 | function last (l) { 24 | return l[l.length - 1] 25 | } 26 | 27 | function passSync(fn) { 28 | return function (data) { 29 | return data.sync ? data : fn(data) 30 | } 31 | } 32 | 33 | exports = module.exports = function (q, cb) { 34 | q = q.filter(Boolean) 35 | if(!q.length) return pull.through() 36 | if(last(q).$reduce && cb) { 37 | return pull.apply(null, 38 | q.slice(0, q.length - 1).map(get) 39 | .concat(exports.reduce(last(q).$reduce, cb)) 40 | ) 41 | } 42 | else if(Array.isArray(q)) 43 | return pull.apply(null, q.map(get)) 44 | else 45 | return get(q) 46 | } 47 | 48 | exports.filter = function (q) { 49 | return pullFilter(passSync(make(q))) 50 | } 51 | 52 | exports.map = function (q) { 53 | return pull(pullMap(passSync(make(q))), pullFilter()) 54 | } 55 | 56 | exports.reduce = function (q, cb) { 57 | //TODO: realtime reduce. 58 | if(cb) 59 | return pullReduce(make(q), null, cb) 60 | return pull(SinkThrough(function (cb) { 61 | return pullReduce(make(q), null, cb) 62 | }), pullFlatten()) 63 | } 64 | 65 | exports.sort = function (paths) { 66 | console.log("SORT", paths) 67 | return pullSort(CompareAt(paths.$sort)) 68 | } 69 | -------------------------------------------------------------------------------- /keys.js: -------------------------------------------------------------------------------- 1 | var u = require('./util') 2 | var isArray = u.isArray 3 | var isString = u.isString 4 | 5 | var extractors = { 6 | $filter: function (query) { 7 | var o = {} 8 | for(var k in query) 9 | o[k] = true 10 | return o 11 | }, 12 | $reduce: function (query) { 13 | var o = {} 14 | if(isArray(query.$group)) 15 | query.$group.forEach(function (path) { 16 | o[isArray(path) ? path[0]: path] = true 17 | }) 18 | //TODO: check what paths the other reduces touch. 19 | 20 | return o 21 | }, 22 | $map: function (query) { 23 | var o = {} 24 | for(var k in query) 25 | if(isArray(query[k])) o[query[k][0]] = true 26 | else if(isString(query[k])) o[query[k]] = true 27 | return o 28 | } 29 | } 30 | 31 | function merge (a, b) { 32 | var o = {} 33 | for(var k in a) 34 | o[k] = true 35 | for(var k in b) 36 | o[k] = true 37 | return o 38 | } 39 | 40 | function first(q) { 41 | for(var k in q) return k 42 | } 43 | 44 | function keys (query) { 45 | var keys = {} 46 | for(var i = query.length - 1; i >= 0; i--) { 47 | var k = first(query[i]) 48 | keys = merge(extractors[k](query[i][k]), keys) 49 | } 50 | return keys 51 | } 52 | 53 | module.exports = keys 54 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | var u = require('./util') 2 | 3 | function isBasicMap (rule) { 4 | if(u.isString(rule)) return function (value) { 5 | return value != null ? value[rule] : undefined 6 | } 7 | //negative numbers access from the end of the list. 8 | if(u.isInteger(rule)) return function (value) { 9 | return rule >= 0 ? value[rule] : value[+value.length + rule] 10 | } 11 | 12 | if(true === rule) 13 | return function (value) { return value } 14 | } 15 | 16 | function isBasicLiteral (rule) { 17 | if(u.isBasic(rule)) 18 | return function (v) { return rule === v } 19 | } 20 | 21 | function isFun (rule) { 22 | if(u.isFunction(rule)) return rule 23 | } 24 | 25 | function isArrayCompose (rule) { 26 | if(u.isArray(rule)) { 27 | var rules = rule.map(make) 28 | return function (value) { 29 | return rules.reduce(function (value, fn) { 30 | return fn(value) 31 | }, value) 32 | } 33 | } 34 | } 35 | 36 | function isUnknown(rule) { 37 | throw new Error('could not process:'+JSON.stringify(rule)) 38 | } 39 | 40 | function map$ (obj, map) { 41 | var $obj = {} 42 | for(var k in obj) 43 | $obj['$'+k] = map(obj[k]) 44 | return $obj 45 | } 46 | 47 | var maps = map$(require('./simple/maps'), function (fn) { 48 | return function (argument) { 49 | return function (value) { 50 | return fn(value, argument) 51 | } 52 | } 53 | }) 54 | 55 | function and (a, b) { 56 | if(!a) return b 57 | return function (val) { 58 | return a(val) && b(val) 59 | } 60 | } 61 | 62 | function isMap(obj) { 63 | var fn 64 | for(var k in obj) 65 | if(maps[k]) fn = and(fn, maps[k].call(make, obj[k])) 66 | return fn 67 | } 68 | 69 | function applyFirst(fns) { 70 | return function (query) { 71 | for(var k in query) 72 | if(fns[k]) return fns[k](query[k], query) 73 | } 74 | } 75 | 76 | var isReduce = applyFirst(map$(require('./simple/reduces'), function (reduce) { 77 | return function (argument) { 78 | var get = make(argument) 79 | return function (a, b) { 80 | return reduce(a, get(b)) 81 | } 82 | } 83 | })) 84 | 85 | var compare = require('typewiselite') 86 | var search = require('binary-search') 87 | 88 | function is$ (obj) { 89 | for(var k in obj) 90 | if(k[0] === '$') return true 91 | return false 92 | } 93 | 94 | //rawpaths, reducedpaths, reduce 95 | function arrayGroup (set, get, reduce) { 96 | 97 | //we can use a different lookup path on the right hand object 98 | //is always the "needle" 99 | function _compare (hay, needle) { 100 | for(var i in set) { 101 | var x = u.get(hay, set[i]), y = needle[i] 102 | if(x !== y) return compare(x, y) 103 | } 104 | return 0 105 | } 106 | 107 | return function (a, b) { 108 | if(a && !Array.isArray(a)) a = reduce([], a) 109 | var A = a = a || [] 110 | var i = search(A, get.map(function (fn) { return fn(b) }), _compare) 111 | 112 | if(i >= 0) A[i] = reduce(A[i], b) 113 | else A.splice(~i, 0, reduce(undefined, b)) 114 | 115 | return a 116 | } 117 | } 118 | 119 | var special = { 120 | filter: function makeFilter (rule) { 121 | if(u.isContainer(rule) && !is$(rule)) { //array or object 122 | rule = u.map(rule, makeFilter) 123 | return function (value) { 124 | if(value == null) return false 125 | for(var k in rule) 126 | if(!rule[k](value[k])) return false 127 | return true 128 | } 129 | } 130 | 131 | //now only values at the end... 132 | return isBasicLiteral(rule) || make(rule) 133 | }, 134 | 135 | map: function makeMap (rule) { 136 | if(u.isObject(rule) && !is$(rule)) { 137 | var rules = u.map(rule, makeMap) 138 | return function (value) { 139 | if(value == null) return undefined 140 | var keys = 0 141 | var ret = u.map(rules, function (fn, key) { 142 | if(rule[key] === true) { 143 | keys ++ 144 | return value && value[key] 145 | } 146 | keys ++ 147 | return fn(value) 148 | }) 149 | return keys ? ret : undefined 150 | } 151 | } 152 | return make(rule) 153 | }, 154 | 155 | reduce: function (rule) { 156 | var gets = [], sets = [] 157 | var reduce = (function makeReduce (rule, path) { 158 | if(u.isObject(rule) && !is$(rule)) { 159 | var rules = u.map(rule, function (rule, k) { 160 | return makeReduce(rule, path.concat(k)) 161 | }) 162 | return function (a, b) { 163 | if(!a) a = {} 164 | return u.map(rules, function (reduce, key) { 165 | return a[key] = reduce.length === 1 166 | ? reduce(b) : reduce(a[key], b) 167 | }) 168 | } 169 | } 170 | else { 171 | var fn = make(rule) 172 | if(fn.length === 1) { gets.push(fn); sets.push(path.length == 1 ? path[0] : path) } 173 | return fn 174 | } 175 | })(rule, []) 176 | 177 | return gets.length 178 | ? arrayGroup(sets, gets, reduce) : reduce 179 | } 180 | } 181 | 182 | var isSpecial = applyFirst(map$(special, function (fn) { 183 | return function (query) { return fn(query) } 184 | })) 185 | 186 | function make (rule) { 187 | return isBasicMap(rule) || isFun(rule) || isArrayCompose(rule) || 188 | isMap(rule) || isReduce(rule) || isSpecial(rule) || isUnknown(rule) 189 | } 190 | 191 | module.exports = make 192 | 193 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "map-filter-reduce", 3 | "description": "", 4 | "version": "3.2.2", 5 | "homepage": "https://github.com/dominictarr/map-filter-reduce", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/dominictarr/map-filter-reduce" 9 | }, 10 | "dependencies": { 11 | "binary-search": "^1.2.0", 12 | "compare-at-paths": "^1.0.0", 13 | "pull-sink-through": "0.0.0", 14 | "pull-sort": "^1.0.1", 15 | "pull-stream": "^3.4.3", 16 | "typewiselite": "^1.0.0" 17 | }, 18 | "devDependencies": { 19 | "tape": "^4.4.0" 20 | }, 21 | "scripts": { 22 | "test": "set -e; for t in test/*.js; do node $t; done" 23 | }, 24 | "author": "Dominic Tarr (http://dominictarr.com)", 25 | "license": "MIT" 26 | } 27 | -------------------------------------------------------------------------------- /simple/maps.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var u = require('../util') 4 | 5 | exports.lt = function (a, b) { 6 | return a < b 7 | } 8 | exports.gt = function (a, b) { 9 | return a > b 10 | } 11 | exports.lte = function (a, b) { 12 | return a <= b 13 | } 14 | exports.gte = function (a, b) { 15 | return a >= b 16 | } 17 | exports.eq = function (a, b) { 18 | return a === b 19 | } 20 | exports.ne = function (a, b) { 21 | return a !== b 22 | } 23 | exports.not = function (a) { 24 | return !a 25 | } 26 | exports.truthy = function (a) { 27 | return !!a 28 | } 29 | exports.prefix = function (a, str) { 30 | if(u.isArray(str)) 31 | return ( u.isArray(a) && a.length > str.length 32 | && str.every(function (v, i) { 33 | return a[i] === v 34 | }) 35 | ) 36 | return 'string' == typeof a && a.substring(0, str.length) == str 37 | } 38 | exports.is = function (a, type) { 39 | return typeof a === type 40 | } 41 | 42 | exports.in = function (a, b) { 43 | return ~b.indexOf(a) 44 | } 45 | 46 | exports.type = function (a) { 47 | return typeof a 48 | } 49 | exports.int = function (a) { 50 | return u.isInteger(a) 51 | } 52 | exports.mod = function (a, b) { 53 | return a % b 54 | } 55 | exports.get = function (a, path) { 56 | return u.get(a, path) 57 | } 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /simple/reduces.js: -------------------------------------------------------------------------------- 1 | 2 | exports.count = function (a, b) { 3 | return (a||0)+1 4 | } 5 | exports.sum = function (a, b) { 6 | return (+a||0)+(+b||0) 7 | } 8 | exports.product = function (a, b) { 9 | return (a||1)*(b||1) 10 | } 11 | exports.max = function (a, b) { 12 | if(b === undefined) a 13 | if(a === undefined) return b 14 | return Math.max(a, b) 15 | } 16 | exports.min = function (a, b) { 17 | if(b === undefined) a 18 | if(a === undefined) return b 19 | return Math.min(a, b) 20 | } 21 | exports.collect = function (a, b) { 22 | if(!a) a = a || [] 23 | if(!Array.isArray(a)) a = [a] 24 | a.push(b) 25 | return a 26 | } 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/filter-ssb.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var make = require('../make') 3 | function filter (q) { 4 | return make({$filter: q}) 5 | } 6 | var input = [ 7 | { 8 | "rel": [ 9 | "vote", 10 | 1 11 | ], 12 | "dest": "%dqEyLcxlGPOL21uRH6mVxOCSjdl3tOmmF0HkdzI2nqs=.sha256", 13 | "source": "@p13zSAiOpguI9nsawkGijsnMfWmFd5rlUNpzekEE+vI=.ed25519", 14 | "ts": 1449201687478.029 15 | }, 16 | { 17 | "rel": [ 18 | "vote", 19 | 1 20 | ], 21 | "dest": "%dqEyLcxlGPOL21uRH6mVxOCSjdl3tOmmF0HkdzI2nqs=.sha256", 22 | "source": "@vt8uK0++cpFioCCBeB3p3jdx4RIdQYJOL/imN1Hv0Wk=.ed25519", 23 | "ts": 1449201705535.0264 24 | } 25 | ] 26 | 27 | tape('foo', function (t) { 28 | t.deepEqual( 29 | input.filter(filter({rel: ['vote', 1]})), 30 | input 31 | ) 32 | t.end() 33 | }) 34 | -------------------------------------------------------------------------------- /test/filter.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var make = require('../make') 3 | 4 | function filter(query) { 5 | return make({$filter: query}) 6 | } 7 | 8 | var data = [ 9 | 'string', 10 | 'strength', 11 | {okay: true}, 12 | {foo: 1, bar: 2}, 13 | {a: {b: 'okay'}}, 14 | {a: {b: 'notokay'}}, 15 | {source: 'a', dest: 'b', rel: ['name', '@bob']}, 16 | {source: 'b', dest: 'a', rel: ['name', '@alice']}, 17 | {source: 'b', dest: 'a', rel: ['contact', true, false]}, 18 | {source: 'b', dest: 'a', rel: ['contact', false, null]}, 19 | {source: 'x', dest: 'a', rel: ['age', false, null]}, 20 | true, 21 | false 22 | ] 23 | 24 | var queries = [ 25 | 'string', 26 | {okay: true}, 27 | {$prefix: 'str'}, 28 | {a: {b: {$gt:'ok'}}}, 29 | {rel: ['name', {$prefix: '@'}]}, 30 | {rel: ['name', {$prefix: '@b'}]}, 31 | {rel: {$prefix: ['contact', true]}}, 32 | {rel: ['contact', {$is: 'boolean'}]}, 33 | {$is: 'boolean'}, 34 | {$is: 'string'}, 35 | {source: {$in: ['x', 'a']}}, 36 | {rel: [{$in: ['name', 'age']}]} 37 | ] 38 | 39 | var expected = [ 40 | ['string'], 41 | [{okay: true}], 42 | ['string', 'strength'], 43 | [{a: {b: 'okay'}}], 44 | [ 45 | {source: 'a', dest: 'b', rel: ['name', '@bob']}, 46 | {source: 'b', dest: 'a', rel: ['name', '@alice']} 47 | ], 48 | [{source: 'a', dest: 'b', rel: ['name', '@bob']}], 49 | [{source: 'b', dest: 'a', rel: ['contact', true, false]}], 50 | [ 51 | {source: 'b', dest: 'a', rel: ['contact', true, false]}, 52 | {source: 'b', dest: 'a', rel: ['contact', false, null]} 53 | ], 54 | [true, false], 55 | ['string', 'strength'], 56 | [ 57 | {source: 'a', dest: 'b', rel: ['name', '@bob']}, 58 | {source: 'x', dest: 'a', rel: ['age', false, null]}, 59 | ], 60 | [ 61 | {source: 'a', dest: 'b', rel: ['name', '@bob']}, 62 | {source: 'b', dest: 'a', rel: ['name', '@alice']}, 63 | {source: 'x', dest: 'a', rel: ['age', false, null]}, 64 | ] 65 | ] 66 | 67 | tape('okay: true', function (t) { 68 | t.ok(filter(queries[1])({okay: true})) 69 | t.end() 70 | }) 71 | 72 | tape('a: { b: >ok }', function (t) { 73 | t.ok(filter(queries[3])({a: {b: 'okay'}})) 74 | t.end() 75 | }) 76 | 77 | queries.forEach(function (q, i) { 78 | tape('test filter: '+JSON.stringify(q), function (t) { 79 | t.deepEqual(data.filter(filter(q)), expected[i]) 80 | t.end() 81 | }) 82 | }) 83 | 84 | 85 | var mfr = require('..') 86 | var pull = require('pull-stream') 87 | queries.forEach(function (q, i) { 88 | tape('test filter: '+JSON.stringify(q), function (t) { 89 | pull( 90 | pull.values(data), 91 | mfr([{$filter: q}]), 92 | pull.collect(function (err, results) { 93 | t.deepEqual(results, expected[i]) 94 | t.end() 95 | }) 96 | ) 97 | }) 98 | }) 99 | 100 | tape('sort', function (t) { 101 | pull( 102 | pull.values(data), 103 | mfr([{ 104 | $filter: { 105 | rel: ['name'] 106 | }}, { 107 | $sort: [ 108 | ['rel', 0], 109 | ['rel', 1], 110 | ['source'], 111 | ['dest'] 112 | ]} 113 | ]), 114 | pull.collect(function (err, results) { 115 | t.deepEqual(results, [ 116 | {source: 'b', dest: 'a', rel: ['name', '@alice']}, 117 | {source: 'a', dest: 'b', rel: ['name', '@bob']} 118 | ]) 119 | t.end() 120 | }) 121 | ) 122 | }) 123 | 124 | -------------------------------------------------------------------------------- /test/flexible.js: -------------------------------------------------------------------------------- 1 | 2 | var make = require('../make') 3 | var tape = require('tape') 4 | 5 | tape('simple', function (t) { 6 | t.equal(make('foo')({foo: true}), true) 7 | t.equal(make(['foo', {$type: true}])({foo: true}), 'boolean') 8 | t.equal(make(['foo', {$type: true}, {$eq: 'boolean'}])({foo: true}), true) 9 | t.equal(make({$gt: 1, $lt: 3})(2), true) 10 | t.equal(make({$gt: 1, $lt: 3})(4), false) 11 | t.equal(make([{$mod: 2}, {$not: true}])(4), true) 12 | 13 | t.end() 14 | }) 15 | 16 | tape('filter, map', function (t) { 17 | 18 | t.equal( 19 | make({$filter: 'foo'}) ('foo'), 20 | true 21 | ) 22 | 23 | t.equal( 24 | make({$filter: {foo: {$lt: 0}, bar: {$gt: 0}}}) 25 | ({foo: -1, bar: 1}), 26 | true 27 | ) 28 | 29 | t.deepEqual( 30 | make({$map: { 31 | foo_lt_1: ['foo', {$lt: 0}], bar_gt2: ['bar', {$gt: 2}] 32 | }}) 33 | ({foo: -1, bar: 1}), 34 | {foo_lt_1: true, bar_gt2: false} 35 | ) 36 | 37 | t.end() 38 | }) 39 | 40 | 41 | tape('reduce', function (t) { 42 | t.deepEqual( 43 | [1,2,3].reduce(make({ 44 | $reduce: { 45 | total: {$sum: true}, 46 | count: {$count: true}, 47 | product: {$product: true} 48 | } 49 | }), null), 50 | {total: 6, count: 3, product: 6} 51 | ) 52 | t.end() 53 | }) 54 | 55 | tape('reduce with group', function (t) { 56 | t.deepEqual( 57 | [1,1.3, 2, 2.6, 3].reduce(make({ 58 | $reduce: { 59 | integer: {$int: 1}, 60 | count: {$count: true}, 61 | product: {$product: true} 62 | } 63 | }), null), 64 | [ 65 | { count: 2, integer: false, product: 3.3800000000000003 }, 66 | { count: 3, integer: true, product: 6 } 67 | ] 68 | ) 69 | t.end() 70 | }) 71 | 72 | -------------------------------------------------------------------------------- /test/groups.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | 3 | var r = require('../make') 4 | var numbers = [1,2,3,4,5] 5 | 6 | function R(query) { 7 | return r({$reduce:query}) 8 | } 9 | 10 | 11 | var groups = [ 12 | { 13 | name: 'pfraze', country: 'US', dwelling: 'apartment' 14 | }, 15 | { 16 | name: 'substack', country: 'US', dwelling: 'house' 17 | }, 18 | { 19 | name: 'mix', country: 'NZ', dwelling: 'house' 20 | }, 21 | { 22 | name: 'du5t', country: 'US', dwelling: 'apartment' 23 | }, 24 | { 25 | name: 'dominic', country: 'NZ', dwelling: 'sailboat' 26 | } 27 | ] 28 | 29 | tape('more groups', function (t) { 30 | t.deepEqual(groups.reduce(R({ 31 | country: 'country', dwelling: 'dwelling', people: {$collect: 'name'} 32 | }), null), [ 33 | { 34 | country: 'NZ', dwelling: 'house', people: ['mix'] 35 | }, 36 | { 37 | country: 'NZ', dwelling: 'sailboat', people: ['dominic'] 38 | }, 39 | { 40 | country: 'US', dwelling: 'apartment', people: ['pfraze', 'du5t'] 41 | }, 42 | { 43 | country: 'US', dwelling: 'house', people: ['substack'] 44 | } 45 | ]) 46 | t.end() 47 | }) 48 | 49 | tape('nested array groups', function (t) { 50 | 51 | t.deepEqual( 52 | groups.reduce(R({ 53 | dwelling: 'dwelling', 54 | citizens: {$reduce: { 55 | name: 'name', country: 'country' 56 | }} 57 | }), null), 58 | [ 59 | {dwelling: 'apartment', citizens: [ 60 | {name: 'du5t', country: 'US'}, 61 | {name: 'pfraze', country: 'US'} 62 | ]}, 63 | {dwelling: 'house', citizens: [ 64 | {name: 'mix', country: 'NZ'}, 65 | {name: 'substack', country: 'US'} 66 | ]}, 67 | {dwelling: 'sailboat', citizens: [ 68 | {name: 'dominic', country: 'NZ'} 69 | ]} 70 | ] 71 | ) 72 | t.end() 73 | }) 74 | 75 | 76 | 77 | tape('primitive properties', function (t) { 78 | return t.end() 79 | t.deepEqual( 80 | groups.reduce(R({ 81 | length: ['name', 'length'], 82 | country: {$count: true} 83 | }), null), 84 | 85 | [ { country: 1, length: 3 }, 86 | { country: 1, length: 4 }, 87 | { country: 1, length: 6 }, 88 | { country: 1, length: 7 }, 89 | { country: 1, length: 8 } ] 90 | ) 91 | 92 | t.end() 93 | }) 94 | 95 | 96 | 97 | 98 | tape('first initial', function (t) { 99 | 100 | t.deepEqual( 101 | groups.reduce(R({ 102 | initial: ['name', 0], 103 | count: {$count: true} 104 | }), null), 105 | [ 106 | { count: 2, initial: 'd' }, 107 | { count: 1, initial: 'm' }, 108 | { count: 1, initial: 'p' }, 109 | { count: 1, initial: 's' } 110 | ] 111 | ) 112 | t.end() 113 | }) 114 | 115 | -------------------------------------------------------------------------------- /test/keys.js: -------------------------------------------------------------------------------- 1 | 2 | var tape = require('tape') 3 | var keys = require('../keys') 4 | //extract the keys used in a query 5 | 6 | var input = [ 7 | [{$filter: {rel: ['mentions', {$prefix: '@d'}]}}], 8 | [{$filter: {rel: ['mentions', {$prefix: ''}], dest: {$prefix: "%"}}}], 9 | [ 10 | {"$filter": { 11 | "dest": {"$prefix": "&"}, 12 | "rel": ["mentions", {"$prefix": ""}, {"$gt": 50000}] 13 | }}, 14 | {"$map": { 15 | "blob": "dest", 16 | "name": ["rel", 1], 17 | "size": ["rel", 2] 18 | }} 19 | ], 20 | [ 21 | {"$filter": { 22 | "rel": ["root"] 23 | }}, 24 | {"$reduce":{ 25 | "$group": ["dest", "source"], "$count": true 26 | }} 27 | ] 28 | ] 29 | 30 | var output = [ 31 | {rel: true}, 32 | {rel: true, dest: true}, 33 | {dest: true, rel: true}, 34 | {rel: true, dest: true, source: true} 35 | ] 36 | 37 | tape('test inputs', function (t) { 38 | for(var i = 0; i < input.length; i++) { 39 | t.deepEqual(keys(input[i]), output[i]) 40 | } 41 | t.end() 42 | }) 43 | 44 | -------------------------------------------------------------------------------- /test/map.js: -------------------------------------------------------------------------------- 1 | 2 | var tape = require('tape') 3 | var make = require('../make') 4 | 5 | var inputs = [ 6 | 'string' 7 | ] 8 | 9 | function map(q, v) { 10 | return make({$map: q})(v) 11 | } 12 | 13 | tape('map', function (t) { 14 | t.deepEqual(map(true, true), true) 15 | t.deepEqual(map(true, false), false) 16 | t.deepEqual(map('key', {key: 1}), 1) 17 | //xxx 18 | t.deepEqual(map({foo: true}, {foo: 1, bar: 2}), {foo: 1}) 19 | t.deepEqual(map({bar: 'bar'}, {foo: 1, bar: 2}), {bar: 2}) 20 | t.deepEqual(map({f: 'bar', b: 'foo'}, {foo: 1, bar: 2}), {f: 2, b: 1}) 21 | t.deepEqual(map( 22 | {foo: true, bar: true}, 23 | {foo:1, bar:2, baz: 3} 24 | ), {foo:1, bar: 2}) 25 | t.deepEqual(map( 26 | {abc: ['a', 'b', 'c', true]}, //{foo: true, bar: true} 27 | {a: {b: {c: {foo:1, bar:2, baz: 3}}}} 28 | ), {abc: {foo:1, bar: 2, baz: 3}}) 29 | t.end() 30 | }) 31 | 32 | tape('array is repeated map', function (t) { 33 | t.deepEqual(map( 34 | ['a', 'b', 'c'], 35 | {a: {b: {c: {foo:1, bar:2}}}} 36 | ), {foo:1, bar: 2}) 37 | t.end() 38 | }) 39 | 40 | tape('badmap', function (t) { 41 | t.deepEqual(map('key', 'string'), undefined) 42 | t.deepEqual(map({foo: true}, {bar:1, baz:2}), {foo: undefined}) 43 | t.deepEqual(map({foo: true}, null), undefined) 44 | t.deepEqual(map(['a', 'b', 'c'], {a: true}), undefined) 45 | t.end() 46 | }) 47 | 48 | -------------------------------------------------------------------------------- /test/project.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var u = require('../util') 3 | var project = u.project 4 | var paths = u.paths 5 | 6 | var input = { 7 | foo: {bar: true, baz: false}, 8 | baz: false, 9 | foofoo: {whatever: false, okay: true} 10 | } 11 | 12 | //extract just the parts of the object that are true, 13 | //or support something that is true. 14 | var expected = { 15 | foo: {bar: true}, 16 | foofoo: {okay: true} 17 | } 18 | 19 | var expected_inverse = 20 | { baz: false, foo: { baz: false }, foofoo: { whatever: false } } 21 | 22 | function isObject (o) { 23 | return o && 'object' === typeof o 24 | } 25 | 26 | 27 | function toMap(test) { 28 | return function (value) { 29 | return test(value) ? value : undefined 30 | } 31 | } 32 | 33 | tape('simple', function (t) { 34 | 35 | t.deepEqual(project(input, toMap(Boolean)), expected) 36 | t.deepEqual(project(input, toMap(function (e) { return e===false })), expected_inverse) 37 | 38 | t.end() 39 | }) 40 | 41 | function truth (e) { return e === true } 42 | function untruth (e) { return e === false } 43 | 44 | function not(fn) { 45 | return function (v) { return !fn(v) } 46 | } 47 | 48 | var input2 = { 49 | foo: {bar: true, baz: false}, 50 | baz: false, 51 | foofoo: {whatever: false, okay: true}, 52 | numbers: [1,2,3] 53 | } 54 | 55 | tape('paths', function (t) { 56 | t.deepEqual(paths(input2, truth), [['foo', 'bar'], ['foofoo', 'okay']]) 57 | t.deepEqual(paths(input2, untruth), [['foo', 'baz'],'baz', ['foofoo', 'whatever']]) 58 | t.deepEqual(paths(input2, u.isObject), []) 59 | t.deepEqual(paths(input2, u.isBasic), [ 60 | [ 'foo', 'bar' ], [ 'foo', 'baz' ], 'baz', 61 | [ 'foofoo', 'whatever' ], [ 'foofoo', 'okay' ] ] 62 | ) 63 | t.deepEqual(paths({okay: true}, truth), ['okay']) 64 | t.deepEqual(paths({okay: true}, untruth), []) 65 | t.end() 66 | }) 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/reduce-stream.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | var tape = require('tape') 3 | 4 | var mfr = require('../') 5 | var data = [ 6 | {key: 1, value: {author: 'alice', vote: 1}, type: 'okay'}, 7 | {key: 2, value: {author: 'bob', vote: 1}, type: 'okay'}, 8 | {key: 3, value: {author: 'bob', vote: -1}, type: 'okay'}, 9 | {key: 4, value: {author: 'alice', vote: 1}, type: 'okay'} 10 | ] 11 | 12 | function test(name, query, result) { 13 | tape(name, function (t) { 14 | pull( 15 | pull.values(data), 16 | mfr.reduce({$reduce: query}), 17 | pull.collect(function (err, ary) { 18 | if(err) throw err 19 | t.deepEqual(ary, result) 20 | t.end() 21 | }) 22 | ) 23 | }) 24 | } 25 | 26 | test('count', {$count: true}, [4]) 27 | test('count, value.author', { 28 | value: {author: ['value', 'author']}, 29 | count: {$count: true}}, [ 30 | {value: {author: 'alice'}, count: 2}, {value: {author: 'bob'}, count: 2} 31 | ]) 32 | 33 | test('count, value.author', { 34 | author: ['value', 'author'], 35 | count: {$count: true}}, [ 36 | {author: 'alice', count: 2}, {author: 'bob', count: 2} 37 | ]) 38 | 39 | test('count, value.author', { 40 | author: ['value', 'author'], 41 | vote: {$sum: ['value', 'vote']}}, [ 42 | {author: 'alice', vote: 2}, {author: 'bob', vote: 0} 43 | ]) 44 | 45 | test('count, value.author', { 46 | author: ['value', 'author'], okay: 'type', 47 | vote: {$sum: ['value', 'vote']}}, [ 48 | {author: 'alice', vote: 2, okay: 'okay'}, {author: 'bob', vote: 0, okay: 'okay'} 49 | ]) 50 | 51 | -------------------------------------------------------------------------------- /test/reduce.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var r = require('../make') 3 | var numbers = [1,2,3,4,5] 4 | 5 | function R(query) { 6 | return r({$reduce:query}) 7 | } 8 | 9 | tape('easy', function (t) { 10 | t.equal(numbers.reduce(R({$count: true})), 5) 11 | t.equal(numbers.reduce(R({$sum: true})), 15) 12 | t.equal(numbers.reduce(R({$max: true})), 5) 13 | t.equal(numbers.reduce(R({$min: true})), 1) 14 | t.deepEqual(numbers.reduce(R({$collect: true})), numbers) 15 | t.end() 16 | }) 17 | 18 | var objs = [ 19 | {foo: 0, bar: 1, baz: false, other: 'sometimes'}, 20 | {foo: 10, bar: 2, baz: true, other: true}, 21 | {foo: -5, bar: 3, baz: true}, 22 | {foo: -5, bar: 4, baz: false, other: {okay: true}}, 23 | {foo: 3, bar: 5, baz: true} 24 | ] 25 | 26 | tape('objects', function (t) { 27 | t.deepEqual( 28 | objs.reduce(R({ 29 | maxFoo: {$max: 'foo'}, 30 | foo: {$sum: 'foo'}, 31 | bar: {$sum: 'bar'} 32 | }), null), 33 | {maxFoo: 10, foo: 3, bar: 15} 34 | ) 35 | 36 | t.end() 37 | }) 38 | 39 | tape('collect', function (t) { 40 | t.deepEqual( 41 | objs.reduce(R({ 42 | primes: {$collect: 'baz'} 43 | }), null), 44 | {primes: [false, true, true, false, true]} 45 | ) 46 | 47 | t.deepEqual( 48 | objs.reduce(R({ 49 | $collect: 'baz' 50 | }), null), 51 | [false, true, true, false, true] 52 | ) 53 | 54 | t.end() 55 | 56 | }) 57 | 58 | tape('group', function (t) { 59 | 60 | t.deepEqual( 61 | objs.reduce(R({ 62 | baz: 'baz', count: {$count: true} 63 | }), null), 64 | [ 65 | {baz: false, count: 2}, 66 | {baz: true, count: 3} 67 | ] 68 | ) 69 | // return t.end() 70 | // t.deepEqual( 71 | // objs.reduce(R({ 72 | // $group: 'baz', $reduce: {foo: {$max:'foo'}, bar: {$sum: 'bar'}} 73 | // }), null), 74 | // {"true": {foo: 10, bar: 10}, "false": {foo: 0, bar: 5}} 75 | // ) 76 | // 77 | // t.deepEqual( 78 | // objs.reduce(R({ 79 | // $group: 'baz', $reduce: {foo: {$max:'foo'}, bar: {$collect: 'bar'}} 80 | // }), null), 81 | // {"true": {foo: 10, bar: [2,3,5]}, "false": {foo: 0, bar: [1,4]}} 82 | // ) 83 | 84 | t.end() 85 | }) 86 | 87 | tape('group, sometimes', function (t) { 88 | 89 | t.deepEqual( 90 | objs.reduce(R({ 91 | other: ['other', 'okay'], 92 | values: {$collect: 'other'} 93 | }), null), 94 | [ 95 | {other: true, values: [{okay: true}]}, 96 | {other: undefined, values: ['sometimes', true, undefined, undefined]} 97 | ] 98 | ) 99 | 100 | t.end() 101 | }) 102 | 103 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var tape = require('tape') 4 | 5 | var u = require('../util') 6 | 7 | var ranges = [ 8 | {$gte: 'a'}, 9 | {$prefix: 'a'}, 10 | {$prefix: ['a', 'b']}, 11 | ['ab', {$prefix: 'c'}] 12 | ] 13 | 14 | tape('u', function (t) { 15 | ranges.map(function (e) { 16 | t.ok(u.isRange(e)) 17 | }) 18 | t.end() 19 | 20 | }) 21 | 22 | tape('ranges', function (t) { 23 | function r(range, upper, lower) { 24 | t.ok(u.isRange(range), JSON.stringify(range) + ' is a range') 25 | t.deepEqual(u.upper(range), upper) 26 | t.deepEqual(u.lower(range), lower) 27 | } 28 | r({$lt: 'a'}, 'a', null) 29 | r({$lte: 'a'}, 'a', null) 30 | r({$gt: 'a'}, undefined, 'a') 31 | r({$gte: 'a'}, undefined, 'a') 32 | 33 | r({$lt: 7}, 7, null) 34 | r({$lte: 7}, 7, null) 35 | r({$gt: 7}, undefined, 7) 36 | r({$gte: 7}, undefined, 7) 37 | 38 | r({$is: 'string'}, [], '') 39 | r({$is: 'boolean'}, true, false) 40 | r({$is: 'array'}, undefined, []) 41 | r({$is: 'undefined'}, undefined, undefined) 42 | r({$is: 'null'}, null, null) 43 | 44 | t.end() 45 | }) 46 | 47 | tape('!isRange', function (t) { 48 | function r(not_range) { 49 | t.notOk(u.isRange(not_range), JSON.stringify(not_range)+ ' is not a range') 50 | } 51 | r({$ne: 'foo'}) 52 | t.end() 53 | }) 54 | tape('!isExact', function (t) { 55 | function r(not_exact) { 56 | t.notOk(u.isExact(not_exact), JSON.stringify(not_exact)+ ' is not exact') 57 | } 58 | r({$ne: 'foo'}) 59 | t.end() 60 | }) 61 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //checkable types 4 | var types = { 5 | string: true, boolean: true, array: true, undefined: true, null: true 6 | } 7 | 8 | var upperByType = { 9 | string: [], 10 | boolean: true, 11 | array: undefined, 12 | undefined: undefined, 13 | null: null 14 | } 15 | var lowerByType = { 16 | string: '', 17 | boolean: false, 18 | array: [], 19 | undefined: undefined, 20 | null: null 21 | } 22 | 23 | function isString(s) { return 'string' === typeof s } 24 | 25 | function isNumber(n) { return !isNaN(+n) } 26 | 27 | var isInteger = Number.isInteger 28 | 29 | function isBoolean (b) { return 'boolean' === typeof b } 30 | 31 | function isBasic (p) { 32 | return isString(p) || isNumber(p) || isBoolean(p) || isNull(p) || isUndefined(p) 33 | } 34 | 35 | function isFunction (f) { return 'function' === typeof f } 36 | 37 | var isArray = Array.isArray 38 | 39 | function isObject (o) { return o && 'object' === typeof o && !isArray(o) } 40 | 41 | function isUndefined (u) { return u === undefined } 42 | function isNull (n) { return n === null } 43 | function isBoolean (b) { return 'boolean' === typeof b } 44 | function isNumber(n) { return 'number' === typeof n} 45 | 46 | // [] or {} 47 | function isContainer (o) { 48 | return o && 'object' == typeof o 49 | } 50 | 51 | function has(o, k) { 52 | return Object.hasOwnProperty.call(o, k) 53 | } 54 | 55 | function isExact (v) { 56 | if(isBasic(v)) return true 57 | if(isArray(v)) 58 | return v.every(isExact) 59 | return isObject(v) && has(v, '$eq') 60 | } 61 | 62 | function isLtgt (v) { 63 | return has(v, '$lt') || has(v, '$gt') || has(v, '$lte') || has(v, '$gte') 64 | } 65 | 66 | function isRange (v) { 67 | if(v == null) return false 68 | // if(!isObject(v)) return false 69 | if(v.$is) console.log(v, types[v.$is], types) 70 | if(v.$prefix || (v.$is && types[v.$is])) return true 71 | if(isArray(v)) return find(v, isRange) 72 | return isLtgt(v) 73 | } 74 | 75 | function find (ary, test) { 76 | for(var i = 0; i < ary.length; i++) 77 | if(test(ary[i], i, ary)) return true 78 | return false 79 | } 80 | 81 | function lower (v) { 82 | if(isBasic(v)) return v 83 | if(isObject(v)) { 84 | if(isArray(v.$prefix)) return v.$prefix.concat(exports.HI) 85 | if(isString(v.$prefix)) return v.$prefix 86 | if(has(v, '$gt')) return v.$gt 87 | if(has(v, '$gte')) return v.$gte 88 | if(has(v, '$lt')) return exports.LO 89 | if(has(v, '$lte')) return exports.LO 90 | if(has(v, '$is')) return lowerByType[v.$is] 91 | } 92 | if(isArray(v)) return v.map(lower) 93 | } 94 | 95 | 96 | function upper (v) { 97 | if(isBasic(v)) return v 98 | if(isObject(v)) { 99 | if(isArray(v.$prefix)) return v.$prefix.concat(exports.LO) 100 | if(isString(v.$prefix)) return v.$prefix+'\uffff' 101 | if(has(v, '$lt')) return v.$lt 102 | if(has(v, '$lte')) return v.$lte 103 | if(has(v, '$gt')) return exports.HI 104 | if(has(v, '$gte')) return exports.HI 105 | 106 | if(has(v, '$is')) return upperByType[v.$is] 107 | 108 | } 109 | if(isArray(v)) return v.map(upper) 110 | } 111 | 112 | function get(obj, path) { 113 | if(isString(path)) return obj[path] 114 | if(isArray(path)) { 115 | for(var i = 0; i < path.length; i++) { 116 | if(obj == null) return undefined 117 | obj = obj[path[i]] 118 | } 119 | return obj 120 | } 121 | if(path === true) return obj 122 | return undefined 123 | } 124 | 125 | function map(obj, iter, o) { 126 | if(Array.isArray(obj)) return obj.map(iter) 127 | o = o || {} 128 | for(var k in obj) 129 | o[k] = iter(obj[k], k, obj) 130 | return o 131 | } 132 | 133 | 134 | function mapa(obj, iter) { 135 | if(Array.isArray(obj)) return obj.map(iter) 136 | var a = [] 137 | for(var k in obj) { 138 | var v = iter(obj[k], k, obj) 139 | if(v !== undefined) a.push(v) 140 | } 141 | return a 142 | 143 | } 144 | 145 | function each(obj, iter) { 146 | if(Array.isArray(obj)) return obj.forEach(iter) 147 | else if(isObject(obj)) 148 | for(var k in obj) iter(obj[k], k, obj) 149 | else 150 | iter(obj) 151 | } 152 | 153 | function project (value, map, isObj) { 154 | isObj = isObj || isObject 155 | if(!isObj(value)) 156 | return map(value) 157 | else { 158 | var o 159 | for(var k in value) { 160 | var v = project(value[k], map, isObj) 161 | if(v !== undefined) 162 | (o = o || {})[k] = v 163 | } 164 | return o 165 | } 166 | } 167 | 168 | //get all paths within an object 169 | //this can probably be optimized to create less arrays! 170 | function paths (object, test) { 171 | var p = [] 172 | if(test(object)) return [] 173 | for(var key in object) { 174 | var value = object[key] 175 | if(test(value)) p.push(key) 176 | else if(isObject(value)) 177 | p = p.concat(paths(value, test).map(function (path) { 178 | return [key].concat(path) 179 | })) 180 | } 181 | return p 182 | } 183 | 184 | exports.isString = isString 185 | exports.isNumber = isNumber 186 | exports.isBoolean = isBoolean 187 | exports.isNull = isNull 188 | exports.isUndefined = isUndefined 189 | exports.isInteger = isInteger 190 | exports.isBasic = isBasic 191 | exports.isArray = isArray 192 | exports.isObject = isObject 193 | exports.isContainer = isContainer 194 | exports.isRange = isRange 195 | exports.isExact = isExact 196 | exports.isLtgt = isLtgt 197 | exports.isFunction = isFunction 198 | 199 | exports.has = has 200 | exports.get = get 201 | exports.map = map 202 | exports.mapa = mapa 203 | exports.project = project 204 | exports.paths = paths 205 | exports.each = each 206 | 207 | exports.upper = upper 208 | exports.lower = lower 209 | 210 | exports.HI = undefined 211 | exports.LO = null 212 | 213 | 214 | 215 | 216 | --------------------------------------------------------------------------------