├── .gitignore ├── Makefile ├── README-rql.md ├── README-schema.md ├── README.md ├── package.json ├── src ├── index.coffee ├── object.coffee ├── rql.coffee └── schema.coffee ├── test ├── facet.js ├── index.html ├── js ├── rql.js ├── schema.js └── webify └── underscore-data.js /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: underscore-data.js 2 | 3 | underscore-data.js: src/index.coffee src/object.coffee src/rql.coffee src/schema.coffee 4 | @coffee -jcp $^ >$@ 5 | 6 | test: underscore-data.js 7 | @cd test ; ./webify 8 | 9 | .PHONY: all test 10 | -------------------------------------------------------------------------------- /README-rql.md: -------------------------------------------------------------------------------- 1 | ## Aim 2 | 3 | To provide simple means to declaratively query recordsets represented by arrays/hashes of JS objects. Consider 4 | 5 | _.query([{a:1,b:2},{a:3,b:2}], '(a=1|b=2)') === [{a:1,b:2},{a:3,b:2}] 6 | _.query([{a:1,b:2},{a:3,b:2}], '(a=1&b=2)') === [{a:1,b:2}] 7 | _.query([{a:1,b:2},{a:3,b:2}], '(a=1&b!=2)') === [] 8 | _.query([{a:1,b:2},{a:3,b:2}], 'a>=1') === [{a:1,b:2},{a:3,b:2}] 9 | 10 | _.query([{a:1,b:2},{a:3,b:2}], 'eq(b,2)&in(a,3,5,7)') === [{a:3,b:2}] 11 | _.query([{a:1,b:2},{a:3,b:2}], 'eq(b,2)&nin(a,3,5,7)') === [{a:1,b:2}] 12 | 13 | _.query([{a:1,b:2},{a:3,b:2}], 'a>=1&sort(-a)') === [{a:3,b:2},{a:1,b:2}] 14 | _.query([{a:1,b:2},{a:3,b:2}], 'a>=1&select(-a)') === [{b:2},{b:2}] 15 | _.query([{a:1,b:2},{a:3,b:2}], 'a>=1&select(-a)&limit(1,1)') === [{b:2}] 16 | _.query([{a:1,b:2},{a:3,b:2}], 'a>=1&limit(1)') === [{a:1,b:2}] 17 | 18 | _.query([{a:1,foo:{bar:'baz'}},{a:3,b:2}], 'foo/bar=re:ba') === [{a:1,foo:{bar:'baz'}}] 19 | 20 | Behind the scene `_.query` is based upon [Resource Query Language](https://github.com/kriszyp/rql). 21 | 22 | The heart of query language is `Query` class, which is instanciated with a call to `_.rql([querystring])`. `Query` can be iteratively tuned by calling its methods: 23 | 24 | _.rql('a=1').eq(b,2).sort('-n').limit(2,1)... 25 | 26 | `Query` provides `.toMongo()` method which returns a hash `{search: {...}, meta: {...}}` suitable to be passed to MongoDB accessors, e.g.: 27 | 28 | _.rql('(a=1|b!=3).sort(-a).select(a).limit(10,5)').toMongo() 29 | 30 | gives 31 | 32 | {search: {$or: [{a:1},{b:{$ne:3}}], meta: {sort:{a:-1}, fields:{a:1}, skip:5, limit:10}}} 33 | 34 | ## Install 35 | 36 | npm install underscore-data 37 | 38 | ## Test 39 | 40 | make 41 | 42 | and point your browser to http://127.0.0.1:8080 43 | -------------------------------------------------------------------------------- /README-schema.md: -------------------------------------------------------------------------------- 1 | ## Aim 2 | 3 | Traditional get/set pattern of using key-value stores doesn't fit secured environment needs since almost always a particular record is not supposed to be exposed in its entirety. Consider a collection of User objects presenting users of a secured website. Ordinary users should have RW access to their profiles, but should have no access to fields defining their capabilities, the latter being objects managed by power administrator users. Also, validating a JS object presenting user input prior to letting it be persisted in the store. 4 | 5 | Thus, simple _practical_ means to validate and filter objects passed to and from various datastores are needed. 6 | 7 | One of the most powerful yet natural ways to represent validation rules is [JSON-Schema](http://json-schema.org/), the tersest yet close to full implementation [here](https://github.com/kriszyp/json-schema), the full list of implementations [here](http://json-schema.org/implementations.html). 8 | 9 | Assumptions are: 10 | 11 | * if one specifies schema in JS source file, he can explicitly specify relations as JS references, so no need in JSON gameplay to express inheritance; 12 | * there are 3 types of operations on data store which require validation: 1) add a new record; 2) modify parts of records -- modifying record as whole is simply an extreme case; 3) fetch records back 13 | * having one slightly more elaborate schema is better than having three schemas 14 | 15 | To illustrate, consider a simple schema representing a User record: 16 | 17 | User = 18 | type: 'object' 19 | properties: 20 | id: 21 | type: 'string' 22 | pattern: /^[A-Za-z0-9_]$/ 23 | # --- authority --- 24 | rights: 25 | type: 'any' 26 | # --- authentication --- 27 | salt: 28 | type: 'string' 29 | password: 30 | type: 'string' 31 | # --- profile --- 32 | email: 33 | type: 'string' 34 | creditcardinfo: 35 | type: 'string' 36 | anotherprivateinfo: 37 | type: 'any' 38 | 39 | 40 | Let us see how one can improve this schema using `veto` attribute to achieve the separation of access: 41 | 42 | UserAsSeenByAdmin = 43 | type: 'object' 44 | additionalProperties: false 45 | properties: 46 | # N.B. no way to change id when updating the record 47 | id: _.extend {}, User.properties.id, veto: {update: true} 48 | # N.B. authority is of full RW access 49 | rights: User.properties.rights 50 | # N.B. admin can specify initial secrets 51 | salt: _.extend {}, User.properties.salt, veto: {update: true, get: true} 52 | password: _.extend {}, User.properties.password, veto: {update: true, get: true} 53 | # N.B. admin can view user email, and cannot neither set initial value, nor update it 54 | email: _.extend {}, User.properties.email, veto: {add: true, update: true} 55 | # N.B. admin has no access to the rest of the record 56 | # ... 57 | 58 | UserAsSeenByUser = 59 | type: 'object' 60 | additionalProperties: false 61 | properties: 62 | # N.B. RO id 63 | id: _.extend {}, User.properties.id, veto: {add: true, update: true} 64 | # N.B. RO authority 65 | rights: _.extend {}, User.properties.rights, veto: {add: true, update: true} 66 | # N.B. update-only secrets -- no need to leak this even for user himself 67 | salt: _.extend {}, User.properties.salt, veto: {add: true, get: true} 68 | password: _.extend {}, User.properties.password, veto: {add: true, get: true} 69 | # N.B. RW email 70 | email: User.properties.email 71 | # N.B. RW access to the rest of the record 72 | creditcardinfo: User.properties.creditcardinfo 73 | # ... 74 | 75 | The `veto` attribute is a boolean, or a hash of four boolean keys: `add`, `update`, `get`, `query` representing the type of operation for which validation is performed. The usage pattern is as follows: 76 | 77 | * to validate INPUT when adding new User record: 78 | 79 | `_.validate INPUT, UserAsSeenByAdmin, flavor: 'add'` 80 | 81 | This will validate INPUT using UserAsSeenByAdmin schema, removing any properties attributed as `veto: {add: true}`, setting default values on missed properties before validation 82 | 83 | * to validate INPUT when updating one/many User records: 84 | 85 | `_.validate INPUT, UserAsSeenByAdmin, flavor: 'update'` 86 | 87 | This will validate INPUT using UserAsSeenByAdmin schema, taking in account only those properties which do exist in INPUT, removing any properties attributed as `veto: {update: true}`, ignoring `default` attribute 88 | 89 | * to validate User records returned from the store: 90 | 91 | `_.map records, (record) -> _.validate record, UserAsSeenByAdmin, flavor: 'get'` 92 | 93 | This will kick off from each of records any properties in UserAsSeenByAdmin schema attributed as `veto: {get: true}` 94 | 95 | The concept of accessing a particular record under different POVs is close to that introduced by `Facet`s in [Perstore](https://github.com/kriszyp/perstore) which are means to separate access at record level based on object capability. 96 | 97 | ## Coercion 98 | 99 | Validation procedure will try to coerce the type of a property to that defined in the schema if `_.validate` is given `coerce: true` option. Coercion is not done for `flavor: 'get'` 100 | 101 | ## Install 102 | 103 | Run: 104 | 105 | make 106 | 107 | ## Example and test 108 | 109 | make 110 | 111 | and point your browser to http://127.0.0.1:8080 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | underscore-data 2 | ------ 3 | 4 | Data querying and validating tools based on underscore.js 5 | 6 | Please look at [rql](https://github.com/dvv/underscore-data/blob/master/README-rql.md) and [schema](https://github.com/dvv/underscore-data/blob/master/README-schema.md) readmes. 7 | 8 | License 9 | ------ 10 | 11 | 12 | Copyright (c) 2012 Vladimir Dronnikov 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy of 15 | this software and associated documentation files (the "Software"), to deal in 16 | the Software without restriction, including without limitation the rights to 17 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 18 | the Software, and to permit persons to whom the Software is furnished to do so, 19 | subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 26 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 27 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 28 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 29 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "underscore-data", 3 | "description" : "Practical tools to manage schemaful mongodb documents", 4 | "url" : "https://github.com/dvv/underscore-data/", 5 | "keywords" : ["json", "schema", "underscore", "javascript", "mongodb", "rql"], 6 | "author" : "Vladimir Dronnikov ", 7 | "contributors" : [], 8 | "dependencies" : {"underscore": ""}, 9 | "lib" : ".", 10 | "main" : "underscore-data.js", 11 | "version" : "0.0.6" 12 | } 13 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | # support ender.js 2 | if this._ is undefined and this.$ isnt undefined and $.ender 3 | this._ = $ 4 | this._.mixin = this.$.ender 5 | -------------------------------------------------------------------------------- /src/object.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | ### 4 | * 5 | * Copyright(c) 2011 Vladimir Dronnikov 6 | * MIT Licensed 7 | * 8 | ### 9 | 10 | # 11 | # various helpers 12 | # 13 | 14 | _.mixin 15 | 16 | # 17 | # naive check if `value` is an object 18 | # 19 | isObject: (value) -> 20 | value and typeof value is 'object' 21 | 22 | # 23 | # ensure passed `value` is an array 24 | # make the array of the single item `value` otherwise 25 | # 26 | ensureArray: (value) -> 27 | return (if value is undefined then [] else [value]) unless value 28 | return [value] if _.isString value 29 | _.toArray value 30 | 31 | # 32 | # converts a `list` of objects to hash keyed by `field` in objects 33 | # 34 | toHash: (list, field) -> 35 | r = {} 36 | _.each list, (x) -> 37 | f = _.drill x, field 38 | r[f] = x 39 | r 40 | 41 | # 42 | # deep freeze an object 43 | # 44 | freeze: (obj) -> 45 | if _.isObject obj 46 | Object.freeze obj 47 | _.each obj, (v, k) -> _.freeze v 48 | obj 49 | 50 | # 51 | # expose enlisted object properties 52 | # 53 | # _.proxy {action: (x) -> DATA, private_stuff: ...}, ['action'] ---> {action: (x) -> DATA} 54 | # _.proxy {deep: {action: (x) -> DATA, private_stuff: ...}}, [['deep','action']] ---> {action: (x) -> DATA} 55 | # _.proxy {deep: {action: (x) -> DATA, private_stuff: ...}}, [[['deep','action'], 'allowed']] ---> {allowed: (x) -> DATA} 56 | # _.proxy {private_stuff: ...}, [[console.log, 'allowed']] ---> {allowed: console.log} 57 | # 58 | proxy: (obj, exposes) -> 59 | facet = {} 60 | _.each exposes, (definition) -> 61 | if _.isArray definition 62 | name = definition[1] 63 | prop = definition[0] 64 | prop = _.drill obj, prop unless _.isFunction prop 65 | else 66 | name = definition 67 | prop = obj[name] 68 | # 69 | facet[name] = prop if prop 70 | Object.freeze facet 71 | 72 | # 73 | # drill down along object properties specified by path 74 | # removes the said property and return mangled object if `remove` is truthy 75 | # 76 | # _.drill({a:{b:{c:[0,2,4]}}},['a','b','c',2]) ---> 4 77 | # TODO: _.drill({a:{b:{$ref:function(attr){return{c:[0,2,4]}[attr];}}}},['a','b','c',2]) ---> 4 78 | # TODO: _.drill({a:{b:{$ref:function(err, result){return next(err, {c:[0,2,4]}[attr]);}}}},['a','b','c',2], next) 79 | # 80 | drill: (obj, path, remove) -> 81 | # path as array specifies drilldown steps 82 | if _.isArray path 83 | if remove 84 | [path..., name] = path 85 | orig = obj 86 | for part, index in path 87 | obj = obj and obj[part] 88 | # TODO: splice for arrays when path is number? 89 | delete obj[name] if obj?[name] 90 | orig 91 | else 92 | for part in path 93 | # FIXME: ?should delegate to _.drill obj, part 94 | obj = obj and obj[part] 95 | obj 96 | # no path means no drill 97 | else if path is undefined 98 | obj 99 | # ordinal path means one drilldown step 100 | else 101 | if remove 102 | # TODO: splice for arrays when path is number? 103 | delete obj[path] 104 | obj 105 | else 106 | obj[path] 107 | 108 | _.mixin 109 | 110 | # 111 | # until every engine supports ECMA5, safe coercing to Date is evil 112 | # 113 | # TODO: consider https://github.com/timrwood/underscore.date 114 | # 115 | parseDate: (value) -> 116 | date = new Date value 117 | return date if _.isDate date 118 | parts = String(value).match /(\d+)/g 119 | new Date(parts[0], ((parts[1] or 1) - 1), (parts[2] or 1)) 120 | isDate: (obj) -> 121 | not not (obj?.getTimezoneOffset and obj.setUTCFullYear and not _.isNaN(obj.getTime())) 122 | -------------------------------------------------------------------------------- /src/rql.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | ### 4 | * 5 | * Copyright(c) 2011 Vladimir Dronnikov 6 | * MIT Licensed 7 | * 8 | ### 9 | 10 | ### 11 | Rewrite of kriszyp's RQL https://github.com/kriszyp/rql 12 | Relies on documentcloud/underscore to normalize JS 13 | ### 14 | 15 | operatorMap = 16 | '=': 'eq' 17 | '==': 'eq' 18 | '>': 'gt' 19 | '>=': 'ge' 20 | '<': 'lt' 21 | '<=': 'le' 22 | '!=': 'ne' 23 | 24 | class Query 25 | 26 | constructor: (query, parameters) -> 27 | 28 | query = '' unless query? 29 | 30 | term = @ 31 | term.name = 'and' 32 | term.args = [] 33 | 34 | topTerm = term 35 | 36 | if _.isObject query 37 | if _.isArray query 38 | topTerm.in 'id', query 39 | return 40 | else if query instanceof Query 41 | #_.extend term, query 42 | #console.log 'term', term 43 | query = query.toString() 44 | else 45 | for own k, v of query 46 | term = new Query() 47 | topTerm.args.push term 48 | term.name = 'eq' 49 | term.args = [k, v] 50 | return 51 | else 52 | throw new URIError 'Illegal query' unless typeof query is 'string' 53 | 54 | query = query.substring(1) if query.charAt(0) is '?' 55 | if query.indexOf('/') >= 0 # performance guard 56 | # convert slash delimited text to arrays 57 | query = query.replace /[\+\*\$\-:\w%\._]*\/[\+\*\$\-:\w%\._\/]*/g, (slashed) -> 58 | '(' + slashed.replace(/\//g, ',') + ')' 59 | 60 | # convert FIQL to normalized call syntax form 61 | query = query.replace /(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)/g, (t, property, operator, value) -> 62 | if operator.length < 3 63 | throw new URIError 'Illegal operator ' + operator unless operator of operatorMap 64 | operator = operatorMap[operator] 65 | else 66 | operator = operator.substring 1, operator.length - 1 67 | operator + '(' + property + ',' + value + ')' 68 | 69 | query = query.substring(1) if query.charAt(0) is '?' 70 | leftoverCharacters = query.replace /(\))|([&\|,])?([\+\*\$\-:\w%\._]*)(\(?)/g, (t, closedParen, delim, propertyOrValue, openParen) -> 71 | if delim 72 | if delim is '&' 73 | op = 'and' 74 | else if delim is '|' 75 | op = 'or' 76 | if op 77 | if not term.name 78 | term.name = op 79 | else if term.name isnt op 80 | throw new Error 'Cannot mix conjunctions within a group, use parenthesis around each set of same conjuctions (& and |)' 81 | if openParen 82 | newTerm = new Query() 83 | newTerm.name = propertyOrValue 84 | newTerm.parent = term 85 | term.args.push newTerm 86 | term = newTerm 87 | else if closedParen 88 | isArray = not term.name 89 | term = term.parent 90 | throw new URIError 'Closing parenthesis without an opening parenthesis' unless term 91 | if isArray 92 | term.args.push term.args.pop().args 93 | else if delim is ',' 94 | if term.args.length is 0 95 | term.args.push '' 96 | term.args.push stringToValue propertyOrValue, parameters 97 | else if propertyOrValue 98 | term.args.push stringToValue propertyOrValue, parameters 99 | '' 100 | throw new URIError 'Opening parenthesis without a closing parenthesis' if term.parent 101 | # any extra characters left over from the replace indicates invalid syntax 102 | throw new URIError 'Illegal character in query string encountered ' + leftoverCharacters if leftoverCharacters 103 | 104 | removeParentProperty = (obj) -> 105 | if obj?.args 106 | delete obj.parent 107 | _.each obj.args, removeParentProperty 108 | obj 109 | 110 | removeParentProperty topTerm 111 | 112 | toString: () -> 113 | if @name is 'and' then _.map(@args, queryToString).join('&') else queryToString @ 114 | 115 | where: (query) -> 116 | @args = @args.concat(new Query(query).args) 117 | @ 118 | 119 | # 120 | # TODO: build SQL 121 | # 122 | toSQL: (options = {}) -> throw Error 'Not implemented' 123 | 124 | # 125 | # build MongoDB structured query 126 | # 127 | toMongo: (options = {}) -> 128 | 129 | walk = (name, terms) -> 130 | search = {} # compiled search conditions 131 | # iterate over terms 132 | _.each terms or [], (term = {}) -> 133 | func = term.name 134 | args = term.args 135 | # ignore bad terms 136 | # N.B. this filters quirky terms such as for ?or(1,2) -- term here is a plain value 137 | return unless func and args 138 | # http://www.mongodb.org/display/DOCS/Querying 139 | # nested terms? -> recurse 140 | if _.isString(args[0]?.name) and _.isArray(args[0].args) 141 | if _.include valid_operators, func 142 | nested = walk func, args 143 | search['$'+func] = nested 144 | # N.B. here we encountered a custom function 145 | #console.log 'CUSTOM', func, args 146 | # ... 147 | # http://www.mongodb.org/display/DOCS/Advanced+Queries 148 | # structured query syntax 149 | else 150 | # handle special functions 151 | if func is 'sort' or func is 'select' or func is 'values' 152 | # sort/select/values affect query options 153 | if func is 'values' 154 | func = 'select' 155 | options.values = true # flag to invoke _.values 156 | #console.log 'ARGS', args 157 | pm = plusMinus[func] 158 | options[func] = {} 159 | # substitute _id for id 160 | args = _.map args, (x) -> if x is 'id' or x is '+id' then '_id' else x 161 | args = _.map args, (x) -> if x is '-id' then '-_id' else x 162 | _.each args, (x, index) -> 163 | x = x.join('.') if _.isArray x 164 | a = /([-+]*)(.+)/.exec x 165 | options[func][a[2]] = pm[(a[1].charAt(0) is '-')*1] * (index+1) 166 | return 167 | else if func is 'limit' 168 | # validate limit() args to be numbers, with sane defaults 169 | limit = args 170 | options.skip = +limit[1] or 0 171 | options.limit = +limit[0] or Infinity 172 | options.needCount = true 173 | return 174 | if func is 'le' 175 | func = 'lte' 176 | else if func is 'ge' 177 | func = 'gte' 178 | # args[0] is the name of the property 179 | key = args[0] 180 | args = args.slice 1 181 | key = key.join('.') if _.isArray key 182 | # prohibit keys started with $ 183 | return if String(key).charAt(0) is '$' 184 | # substitute _id for id 185 | key = '_id' if key is 'id' 186 | # the rest args are parameters to func() 187 | if _.include requires_array, func 188 | args = args[0] 189 | # match on regexp means equality 190 | else if func is 'match' 191 | func = 'eq' 192 | regex = new RegExp 193 | regex.compile.apply regex, args 194 | args = regex 195 | else 196 | # FIXME: do we really need to .join()?! 197 | args = if args.length is 1 then args[0] else args.join() 198 | # regexp inequality means negation of equality 199 | func = 'not' if func is 'ne' and _.isRegExp args 200 | # valid functions are prepended with $ 201 | if _.include valid_funcs, func 202 | func = '$'+func 203 | else 204 | #console.log 'CUSTOM', func, valid_funcs, args 205 | # N.B. here we encountered a custom function 206 | return 207 | # ids must be converted to ObjectIDs 208 | if Query.convertId and key is '_id' 209 | if _.isArray args 210 | args = args.map (x) -> Query.convertId x 211 | else 212 | args = Query.convertId args 213 | # $or requires an array of conditions 214 | #console.log 'COND', search, name, key, func, args 215 | if name is 'or' 216 | search = [] unless _.isArray search 217 | x = {} 218 | if func is '$eq' 219 | x[key] = args 220 | else 221 | y = {} 222 | y[func] = args 223 | x[key] = y 224 | search.push x 225 | # other functions pack conditions into object 226 | else 227 | # several conditions on the same property are merged into the single object condition 228 | search[key] = {} if search[key] is undefined 229 | search[key][func] = args if _.isObject(search[key]) and not _.isArray(search[key]) 230 | # equality flushes all other conditions 231 | search[key] = args if func is '$eq' 232 | return 233 | # TODO: add support for query expressions as Javascript 234 | # TODO: add support for server-side functions 235 | #console.log 'OUT', search 236 | search 237 | 238 | search = walk @name, @args 239 | #console.log meta: options, search: search, terms: query 240 | if options.select 241 | options.fields = options.select 242 | delete options.select 243 | result = 244 | meta: options, search: search 245 | result.error = @error if @error 246 | result 247 | 248 | stringToValue = (string, parameters) -> 249 | converter = converters.default 250 | if string.charAt(0) is '$' 251 | param_index = parseInt(string.substring(1), 10) - 1 252 | return if param_index >= 0 and parameters then parameters[param_index] else undefined 253 | if string.indexOf(':') >= 0 254 | parts = string.split ':', 2 255 | converter = converters[parts[0]] 256 | throw new URIError 'Unknown converter ' + parts[0] unless converter 257 | string = parts[1] 258 | converter string 259 | 260 | queryToString = (part) -> 261 | if _.isArray part 262 | mapped = _.map part, (arg) -> queryToString arg 263 | '(' + mapped.join(',') + ')' 264 | else if part and part.name and part.args 265 | mapped = _.map part.args, (arg) -> queryToString arg 266 | part.name + '(' + mapped.join(',') + ')' 267 | else 268 | encodeValue part 269 | 270 | encodeString = (s) -> 271 | if _.isString s 272 | s = encodeURIComponent s 273 | s = s.replace('(','%28').replace(')','%29') if s.match /[\(\)]/ 274 | s 275 | 276 | encodeValue = (val) -> 277 | if val is null 278 | return 'null' 279 | else if typeof val is 'undefined' 280 | return val 281 | if val isnt converters.default('' + (val.toISOString and val.toISOString() or val.toString())) 282 | if _.isRegExp val 283 | # TODO: control whether to we want simpler glob() style 284 | val = val.toString() 285 | i = val.lastIndexOf '/' 286 | type = if val.substring(i).indexOf('i') >= 0 then 're' else 'RE' 287 | val = encodeString val.substring(1, i) 288 | encoded = true 289 | else if _.isDate val 290 | type = 'epoch' 291 | val = val.getTime() 292 | encoded = true 293 | else if _.isString type 294 | type = 'string' 295 | val = encodeString val 296 | encoded = true 297 | else 298 | # FIXME: not very robust 299 | type = typeof val 300 | val = [type, val].join ':' 301 | val = encodeString val if not encoded and _.isString val 302 | val 303 | 304 | autoConverted = 305 | 'true': true 306 | 'false': false 307 | 'null': null 308 | 'undefined': undefined 309 | 'Infinity': Infinity 310 | '-Infinity': -Infinity 311 | 312 | # 313 | # FIXME: should reuse coerce() from validate.coffee? 314 | # 315 | converters = 316 | auto: (string) -> 317 | if string of autoConverted 318 | return autoConverted[string] 319 | number = +string 320 | if _.isNaN(number) or number.toString() isnt string 321 | string = decodeURIComponent string 322 | return string 323 | number 324 | number: (x) -> 325 | number = +x 326 | throw new URIError 'Invalid number ' + x if _.isNaN number 327 | number 328 | epoch: (x) -> 329 | date = new Date +x 330 | throw new URIError 'Invalid date ' + x unless _.isDate date 331 | date 332 | isodate: (x) -> 333 | # four-digit year 334 | date = '0000'.substr(0, 4-x.length) + x 335 | # pattern for partial dates 336 | date += '0000-01-01T00:00:00Z'.substring date.length 337 | converters.date date 338 | date: (x) -> 339 | isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec x 340 | if isoDate 341 | date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6])) 342 | else 343 | date = _.parseDate x 344 | throw new URIError 'Invalid date ' + x unless _.isDate date 345 | date 346 | boolean: (x) -> 347 | #x is 'true' 348 | if x is 'false' then false else not not x 349 | string: (string) -> 350 | decodeURIComponent string 351 | re: (x) -> 352 | new RegExp decodeURIComponent(x), 'i' 353 | RE: (x) -> 354 | new RegExp decodeURIComponent(x) 355 | glob: (x) -> 356 | s = decodeURIComponent(x).replace /([\\|\||\(|\)|\[|\{|\^|\$|\*|\+|\?|\.|\<|\>])/g, (x) -> '\\'+x 357 | s = s.replace(/\\\*/g,'.*').replace(/\\\?/g,'.?') 358 | s = if s.substring(0,2) isnt '.*' then '^'+s else s.substring(2) 359 | s = if s.substring(s.length-2) isnt '.*' then s+'$' else s.substring(0, s.length-2) 360 | new RegExp s, 'i' 361 | 362 | converters.default = converters.auto 363 | 364 | # 365 | # 366 | # 367 | _.each ['eq', 'ne', 'le', 'ge', 'lt', 'gt', 'between', 'in', 'nin', 'contains', 'ncontains', 'or', 'and'], (op) -> 368 | Query.prototype[op] = (args...) -> 369 | @args.push 370 | name: op 371 | args: args 372 | @ 373 | 374 | parse = (query, parameters) -> 375 | #q = new Query query, parameters 376 | #return q 377 | try 378 | q = new Query query, parameters 379 | catch x 380 | q = new Query 381 | q.error = x.message 382 | q 383 | 384 | # 385 | # MongoDB 386 | # 387 | # valid funcs 388 | valid_funcs = ['eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'not', 'mod', 'all', 'size', 'exists', 'type', 'elemMatch'] 389 | # funcs which definitely require array arguments 390 | requires_array = ['in', 'nin', 'all', 'mod'] 391 | # funcs acting as operators 392 | valid_operators = ['or', 'and', 'not'] #, 'xor'] 393 | # 394 | plusMinus = 395 | # [plus, minus] 396 | sort: [1, -1] 397 | select: [1, 0] 398 | 399 | ################################################# 400 | # 401 | # js-array 402 | # 403 | ###### 404 | 405 | jsOperatorMap = 406 | 'eq' : '===' 407 | 'ne' : '!==' 408 | 'le' : '<=' 409 | 'ge' : '>=' 410 | 'lt' : '<' 411 | 'gt' : '>' 412 | 413 | operators = 414 | 415 | and: (obj, conditions...) -> 416 | for cond in conditions 417 | obj = cond(obj) if _.isFunction cond 418 | obj 419 | 420 | or: (obj, conditions...) -> 421 | list = [] 422 | for cond in conditions 423 | list = list.concat(cond(obj)) if _.isFunction cond 424 | _.uniq list 425 | 426 | limit: (list, limit, start = 0) -> 427 | list.slice start, start + limit 428 | 429 | slice: (list, start = 0, end = Infinity) -> 430 | list.slice start, end 431 | 432 | pick: (list, props...) -> 433 | # compose select hash 434 | include = [] 435 | exclude = [] 436 | _.each props, (x, index) -> 437 | leading = if _.isArray x then x[0] else x 438 | a = /([-+]*)(.+)/.exec leading 439 | if _.isArray x then x[0] = a[2] else x = a[2] 440 | if a[1].charAt(0) is '-' 441 | exclude.push x 442 | else 443 | include.push x 444 | # run filter 445 | #console.log 'SELECT', include, exclude 446 | _.map list, (item) -> 447 | # handle inclusion 448 | if _.isEmpty include 449 | selected = _.clone item 450 | else 451 | selected = {} 452 | for x in include 453 | value = _.drill item, x 454 | #console.log 'DRILLING', x, value 455 | continue if value is undefined 456 | if _.isArray x 457 | t = s = selected 458 | n = x.slice(-1) 459 | for i in x 460 | t[i] ?= {} 461 | s = t 462 | t = t[i] 463 | s[n] = value 464 | else 465 | selected[x] = value 466 | #console.log 'INCLUDED', selected 467 | # handle exclusion 468 | for x in exclude 469 | #console.log '-DRILLING', x 470 | _.drill selected, x, true 471 | selected 472 | 473 | values: () -> 474 | _.map operators.pick.apply(@, arguments), _.values 475 | 476 | sort: (list, props...) -> 477 | order = [] 478 | _.each props, (x, index) -> 479 | leading = if _.isArray x then x[0] else x 480 | a = /([-+]*)(.+)/.exec leading 481 | if _.isArray x then x[0] = a[2] else x = a[2] 482 | if a[1].charAt(0) is '-' 483 | order.push 484 | attr: x 485 | order: -1 486 | else 487 | order.push 488 | attr: x 489 | order: 1 490 | # run sort 491 | #console.log 'ORDER', order 492 | list.sort (a, b) -> 493 | for prop in order 494 | #console.log 'COMPARE?', a, b, prop 495 | va = _.drill a, prop.attr 496 | vb = _.drill b, prop.attr 497 | #console.log 'COMPARE!', va, vb, prop 498 | return if va > vb then prop.order else -prop.order if va isnt vb 499 | 0 500 | 501 | match: (list, prop, regex) -> 502 | regex = new RegExp regex, 'i' unless _.isRegExp regex 503 | _.select list, (x) -> regex.test _.drill x, prop 504 | 505 | nmatch: (list, prop, regex) -> 506 | regex = new RegExp regex, 'i' unless _.isRegExp regex 507 | _.select list, (x) -> not regex.test _.drill x, prop 508 | 509 | in: (list, prop, values) -> 510 | values = _.ensureArray values 511 | _.select list, (x) -> _.include values, _.drill x, prop 512 | 513 | nin: (list, prop, values) -> 514 | values = _.ensureArray values 515 | _.select list, (x) -> not _.include values, _.drill x, prop 516 | 517 | contains: (list, prop, value) -> 518 | _.select list, (x) -> _.include _.drill(x, prop), value 519 | 520 | ncontains: (list, prop, value) -> 521 | _.select list, (x) -> not _.include _.drill(x, prop), value 522 | 523 | between: (list, prop, minInclusive, maxExclusive) -> 524 | _.select list, (x) -> minInclusive <= _.drill(x, prop) < maxExclusive 525 | 526 | nbetween: (list, prop, minInclusive, maxExclusive) -> 527 | _.select list, (x) -> not (minInclusive <= _.drill(x, prop) < maxExclusive) 528 | 529 | operators.select = operators.pick 530 | operators.out = operators.nin 531 | operators.excludes = operators.ncontains 532 | operators.distinct = _.uniq 533 | 534 | # 535 | # stringification helper 536 | # 537 | stringify = (str) -> '"' + String(str).replace(/"/g, '\\"') + '"' 538 | 539 | # N.B. you should clone the array if you sort, since sorting affects the original 540 | query = (list, query, options = {}) -> 541 | 542 | #console.log 'QUERY?', query 543 | query = parse query, options.parameters 544 | # parse error -- don't hesitate, return empty array 545 | return [] if query.error 546 | #console.log 'QUERY!', query 547 | 548 | queryToJS = (value) -> 549 | if _.isObject(value) and not _.isRegExp(value) # N.B. V8 treats regexp as function... 550 | # FIXME: object and array simultaneously?! 551 | if _.isArray value 552 | '[' + _.map(value, queryToJS) + ']' 553 | else 554 | if value.name of jsOperatorMap 555 | # item['foo.bar'] ==> item?.foo?.bar 556 | path = value.args[0] 557 | prm = value.args[1] 558 | item = 'item' 559 | if prm is undefined 560 | prm = path 561 | else if _.isArray path 562 | escaped = [] 563 | for p in path 564 | escaped.push stringify p 565 | item += '&&item[' + escaped.join('][') + ']' 566 | else 567 | item += '&&item[' + stringify(path) + ']' 568 | testValue = queryToJS prm 569 | # N.B. regexp equality means match, inequality -- no match 570 | if _.isRegExp testValue 571 | condition = testValue + ".test(#{item})" 572 | if value.name isnt 'eq' 573 | condition = "!(#{condition})" 574 | else 575 | condition = item + jsOperatorMap[value.name] + testValue 576 | #"_.select(list,function(item){return #{condition}})" 577 | "function(list){return _.select(list,function(item){return #{condition};});}" 578 | else if value.name of operators 579 | #"operators.#{value.name}(" + ['list'].concat(_.map(value.args, queryToJS)).join(',') + ')' 580 | "function(list){return operators['#{value.name}'](" + ['list'].concat(_.map(value.args, queryToJS)).join(',') + ');}' 581 | else 582 | # unknown function -- don't hesitate, return empty 583 | #"function(){return []}" 584 | "function(list){return _.select(list,function(item){return false;});}" 585 | else 586 | # escape strings 587 | if _.isString value then stringify(value) else value 588 | 589 | #expr = ';(function(list){return ' + queryToJS(query) + '})(list);' 590 | expr = queryToJS(query).slice(15, -1) # strip the outmost function(list) ... 591 | #console.log expr #, list 592 | if list then (new Function 'list, operators', expr) list, operators else expr 593 | 594 | # 595 | # expose 596 | # 597 | _.mixin 598 | rql: parse 599 | query: query 600 | -------------------------------------------------------------------------------- /src/schema.coffee: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | ### 4 | 5 | JSONSchema Validator - Validates JavaScript objects using JSON Schemas 6 | (http://www.json.com/json-schema-proposal/) 7 | 8 | Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com) 9 | Copyright (c) 2011 Vladimir Dronnikov dronnikov@gmail.com 10 | 11 | Licensed under the MIT (MIT-LICENSE.txt) license 12 | 13 | ### 14 | 15 | ### 16 | * 17 | * Copyright(c) 2011 Vladimir Dronnikov 18 | * MIT Licensed 19 | * 20 | ### 21 | 22 | ### 23 | Rewrite of kriszyp's json-schema validator https://github.com/kriszyp/json-schema 24 | Relies on documentcloud/underscore to normalize JS 25 | ### 26 | 27 | # 28 | # we allow property definition to contain `veto` attribute to control whether to retain the property after validation 29 | # if it's === true -- the property will be deleted 30 | # if it is a hash, it specifies the flavors of validation ('add', 'update', 'get', 'query') when the property is deleted 31 | # 32 | # E.g. veto: {get: true} means when validation is called with truthy options.veto and options.flavor === 'get', the property will be deleted 33 | # 34 | 35 | # 36 | # given `value`, try to coerce it to `type` 37 | # 38 | # FIXME: we should skip conversion if type is matched? 39 | # 40 | coerce = (value, type) -> 41 | if type is 'string' 42 | value = if value? then String(value) else '' 43 | else if type in ['number', 'integer'] 44 | unless _.isNaN value 45 | value = Number value 46 | value = Math.floor value if type is 'integer' 47 | else if type is 'boolean' 48 | value = if value is 'false' then false else not not value 49 | else if type is 'null' 50 | value = null 51 | else if type is 'object' 52 | # can't really think of any sensible coercion to an object 53 | if JSON?.parse 54 | try 55 | value = JSON.parse value 56 | catch err 57 | else if type is 'array' 58 | value = _.ensureArray value 59 | else if type is 'date' 60 | date = _.parseDate value 61 | value = date if _.isDate date 62 | value 63 | 64 | # 65 | # N.B. since we allow "enum" attribute to be async, the whole validator is treated as async if callback is specified 66 | # 67 | # we allow type coercion if options.coerce 68 | # 69 | 70 | # 71 | # N.B. properties are by default required, use `optional: true` to override 72 | # 73 | 74 | # 75 | # N.B. we introduce `value` attribute which fixes the value of the property 76 | # 77 | 78 | # 79 | # TODO: introduce rename attribute -- id ---!get---> _id ---get---> id 80 | # 81 | 82 | validate = (instance, schema, options = {}, callback) -> 83 | 84 | # save the context 85 | self = @ 86 | 87 | # FIXME: what it is? 88 | _changing = options.changing 89 | 90 | # pending validators 91 | asyncs = [] 92 | 93 | # collected errors 94 | errors = [] 95 | 96 | # validate a value against a property definition 97 | checkProp = (value, schema, path, i) -> 98 | 99 | if path 100 | if _.isNumber i 101 | path += '[' + i + ']' 102 | else if i is undefined 103 | path += '' 104 | else 105 | path += '.' + i 106 | else 107 | path += i 108 | 109 | addError = (message) -> 110 | errors.push property: path, message: message 111 | 112 | if (typeof schema isnt 'object' or _.isArray schema) and (path or typeof schema isnt 'function') and not schema?.type 113 | if _.isFunction schema 114 | addError 'type' unless value instanceof schema 115 | else if schema 116 | addError 'invalid' 117 | return null 118 | 119 | if _changing and schema.readonly 120 | addError 'readonly' 121 | 122 | if schema.extends # if it extends another schema, it must pass that schema as well 123 | checkProp value, schema.extends, path, i 124 | 125 | # validate a value against a type definition 126 | checkType = (type, value) -> 127 | if type 128 | # TODO: coffee-ize, underscore-ize 129 | if typeof type is 'string' and type isnt 'any' and 130 | `(type == 'null' ? value !== null : typeof value !== type) && 131 | !(type === 'array' && _.isArray(value)) && 132 | !(type === 'date' && _.isDate(value)) && 133 | !(type === 'integer' && value%1===0)` 134 | return [property: path, message: 'type'] 135 | if _.isArray type 136 | # a union type 137 | unionErrors = [] 138 | for t in type 139 | unionErrors = checkType t, value 140 | break unless unionErrors.length 141 | return unionErrors if unionErrors.length 142 | else if typeof type is 'object' 143 | priorErrors = errors 144 | errors = [] 145 | checkProp value, type, path 146 | theseErrors = errors 147 | errors = priorErrors 148 | return theseErrors 149 | [] 150 | 151 | if value is undefined 152 | if (not schema.optional or typeof schema.optional is 'object' and not schema.optional[options.flavor]) and not schema.get and not schema.default? 153 | addError 'required' 154 | else 155 | errors = errors.concat checkType schema.type, value 156 | if schema.disallow and not checkType(schema.disallow, value).length 157 | addError 'disallowed' 158 | if value isnt null 159 | if _.isArray value 160 | if schema.items 161 | itemsIsArray = _.isArray schema.items 162 | propDef = schema.items 163 | for v, i in value 164 | if itemsIsArray 165 | propDef = schema.items[i] 166 | if options.coerce and propDef.type 167 | value[i] = coerce v, propDef.type 168 | errors.concat checkProp v, propDef, path, i 169 | if schema.minItems and value.length < schema.minItems 170 | addError 'minItems' 171 | if schema.maxItems and value.length > schema.maxItems 172 | addError 'maxItems' 173 | else if schema.properties or schema.additionalProperties 174 | errors.concat checkObj value, schema.properties, path, schema.additionalProperties 175 | if _.isString value 176 | if schema.pattern and not value.match schema.pattern 177 | addError 'pattern' 178 | if schema.maxLength and value.length > schema.maxLength 179 | addError 'maxLength' 180 | if schema.minLength and value.length < schema.minLength 181 | addError 'minLength' 182 | if schema.minimum isnt undefined and typeof value is typeof schema.minimum and schema.minimum > value 183 | addError 'minimum' 184 | if schema.maximum isnt undefined and typeof value is typeof schema.maximum and schema.maximum < value 185 | addError 'maximum' 186 | if schema.enum 187 | enumeration = schema.enum 188 | # if function specified, distinguish between async and sync flavors 189 | if _.isFunction enumeration 190 | # async validator 191 | if enumeration.length is 2 192 | asyncs.push value: value, path: path, fetch: enumeration 193 | # sync validator 194 | else if enumeration.length is 1 195 | addError 'enum' unless enumeration.call(self, value) 196 | # sync getter 197 | else 198 | enumeration = enumeration.call self 199 | addError 'enum' unless _.include enumeration, value 200 | else 201 | # simple array 202 | addError 'enum' unless _.include enumeration, value 203 | if _.isNumber(schema.maxDecimal) and (new RegExp("\\.[0-9]{#{(schema.maxDecimal+1)},}")).test value 204 | addError 'digits' 205 | null 206 | 207 | # validate an object against a schema 208 | checkObj = (instance, objTypeDef = {}, path, additionalProp) -> 209 | 210 | if _.isObject objTypeDef 211 | if typeof instance isnt 'object' or _.isArray instance 212 | errors.push property: path, message: 'type' 213 | for own i, propDef of objTypeDef 214 | value = instance[i] 215 | # set the value unconditionally if 'value' attribute specified, if 'add' and 'update' flavors 216 | if 'value' of propDef and options.flavor in ['add', 'update'] 217 | value = instance[i] = propDef.value 218 | # skip _not_ specified properties 219 | continue if value is undefined and options.existingOnly 220 | # veto props 221 | if options.veto and (propDef.veto is true or typeof propDef.veto is 'object' and propDef.veto[options.flavor]) 222 | delete instance[i] 223 | continue 224 | # done with validation if it is called for 'get' or 'query' and no coercion needed 225 | continue if options.flavor in ['query', 'get'] and not options.coerce 226 | # set default if validation called for 'add' 227 | if value is undefined and propDef.default? and options.flavor is 'add' 228 | value = instance[i] = propDef.default 229 | # throw undefined properties, unless 'add' flavor 230 | if value is undefined and options.flavor isnt 'add' 231 | delete instance[i] 232 | continue 233 | # coerce if coercion is enabled and value is not undefined 234 | if options.coerce and propDef.type and i of instance and value isnt undefined 235 | value = coerce value, propDef.type 236 | instance[i] = value 237 | # remove undefined properties if they are optional 238 | if value is undefined and propDef.optional 239 | delete instance[i] 240 | continue 241 | # 242 | checkProp value, propDef, path, i 243 | 244 | for i, value of instance 245 | if i of instance and not objTypeDef[i] and (additionalProp is false or options.removeAdditionalProps) 246 | if options.removeAdditionalProps 247 | delete instance[i] 248 | continue 249 | else 250 | errors.push property: path, message: 'unspecifed' 251 | requires = objTypeDef[i]?.requires 252 | if requires and not requires of instance 253 | errors.push property: path, message: 'requires' 254 | # N.B. additional properties are validated only if schema is specified in additionalProperties 255 | # otherwise they just go intact 256 | if additionalProp?.type and not objTypeDef[i] 257 | # coerce if coercion is enabled 258 | if options.coerce and additionalProp.type 259 | value = coerce value, additionalProp.type 260 | instance[i] = value 261 | checkProp value, additionalProp, path, i 262 | if not _changing and value?.$schema 263 | errors = errors.concat checkProp value, value.$schema, path, i 264 | errors 265 | 266 | if schema 267 | checkProp instance, schema, '', _changing or '' 268 | 269 | if not _changing and instance?.$schema 270 | checkProp instance, instance.$schema, '', '' 271 | 272 | # TODO: extend async validators to query the property values? 273 | 274 | # run async validators, if any 275 | len = asyncs.length 276 | if callback and len 277 | for async, i in asyncs 278 | do (async) -> 279 | async.fetch.call self, async.value, (err) -> 280 | if err 281 | errors.push property: async.path, message: 'enum' 282 | len -= 1 283 | # proceed when async validators are done 284 | unless len 285 | callback errors.length and errors or null, instance 286 | else if callback 287 | callback errors.length and errors or null, instance 288 | else 289 | return errors.length and errors or null 290 | 291 | return 292 | 293 | # 294 | # expose 295 | # 296 | _.mixin 297 | coerce: coerce 298 | validate: validate 299 | -------------------------------------------------------------------------------- /test/facet.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | module('Object'); 4 | 5 | test('drill-down', function(){ 6 | var obj = {a:{b:{c:{d:[1,2,3]}}}}; 7 | deepEqual(_.drill(obj, ['a','b','c','d',0]), 1, 'get deep property'); 8 | deepEqual(_.drill(obj), obj, 'get deep property'); 9 | deepEqual(_.drill(obj, [undefined, undefined, undefined]), undefined, 'get deep property'); 10 | deepEqual(_.drill(obj, ['a', 'non', 'existing']), undefined, 'get deep property'); 11 | deepEqual(_.drill(_.clone(obj), ['a','b','c','d',1], true), {a:{b:{c:{d:[1,undefined,3]}}}}, 'remove deep property'); 12 | deepEqual(_.drill(_.clone(obj), ['a','non','existing','d',1], true), obj, 'remove deep property'); 13 | }); 14 | 15 | test('toHash', function(){ 16 | var obj = [{a:1,b:2,c:{d:'foo'}},{a:2,b:3,c:{d:'bar'}},{a:3,b:4,c:{d:'baz'}}]; 17 | deepEqual(_.toHash(obj, 'a'), {1:{a:1,b:2,c:{d:'foo'}},2:{a:2,b:3,c:{d:'bar'}},3:{a:3,b:4,c:{d:'baz'}}}, 'shallow toHash'); 18 | deepEqual(_.toHash(obj, ['c','d']), {foo:{a:1,b:2,c:{d:'foo'}},bar:{a:2,b:3,c:{d:'bar'}},baz:{a:3,b:4,c:{d:'baz'}}}, 'deep toHash'); 19 | }); 20 | 21 | if (Object.freeze) { 22 | test('proxy', function(){ 23 | var obj = {action: function(x){return 'acted';}, deep: {action: function(x){return 'acted from deep';}}, private: function(){return 'hidden';}}; 24 | deepEqual(_.proxy(obj, ['action']), {action: obj.action}, 'simple'); 25 | deepEqual(_.proxy(obj, [['deep', 'action']]), {action: obj.deep}, 'named'); 26 | deepEqual(_.proxy(obj, [[['deep', 'action'], 'action']]), {action: obj.deep.action}, 'deep and named'); 27 | equal(_.proxy(obj, [[['deep', 'action'], 'action']]).action(), 'acted from deep', 'deep and named'); 28 | deepEqual(_.proxy(obj, [[console.log, 'log']]), {log: console.log}, 'renamed foreign method'); 29 | }); 30 | 31 | test('shallow frozen with Object.freeze', function(){ 32 | var obj = {a:{b:{c:{d:[1,2,3]}}}}; 33 | Object.freeze(obj); 34 | obj.a = 1; 35 | deepEqual(obj, {a:{b:{c:{d:[1,2,3]}}}}, 'shallow'); 36 | obj.a.b.c.d.push(4); 37 | deepEqual(obj, {a:{b:{c:{d:[1,2,3,4]}}}}, 'shallow'); 38 | obj.a.b = 1; 39 | deepEqual(obj, {a:{b:1}}, 'shallow'); 40 | }); 41 | 42 | test('deeply (really) frozen with _.freeze', function(){ 43 | var obj = {a:{b:{c:{d:[1,2,3]}}}}; 44 | _.freeze(obj); 45 | try { obj.a = 1; } catch (x){} 46 | deepEqual(obj, {a:{b:{c:{d:[1,2,3]}}}}, 'deep'); 47 | try { obj.a.b.c.d.push(4); } catch (x){} 48 | deepEqual(obj, {a:{b:{c:{d:[1,2,3]}}}}, 'deep'); 49 | try { obj.a.b = 1; } catch (x){} 50 | deepEqual(obj, {a:{b:{c:{d:[1,2,3]}}}}, 'deep'); 51 | }); 52 | } 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

Simple Test Suite

18 |

19 |

20 |
    21 |
    22 | 23 | 24 | -------------------------------------------------------------------------------- /test/js: -------------------------------------------------------------------------------- 1 | .. -------------------------------------------------------------------------------- /test/rql.js: -------------------------------------------------------------------------------- 1 | function testSchema(data){ 2 | data = data.postalcodes; 3 | //console.log('TEST', data); 4 | test("real data -- geonames of postal code 17000", function(){ 5 | equal(_.query(data, 'countryCode=CZ').length, 4); 6 | equal(_.query(data, _.rql().eq('placeName','Holešovice (část)')).length, 1); 7 | deepEqual(_.query(data, _.rql('placeName=Hole%C5%A1ovice%20%28%C4%8D%C3%A1st%29,values(adminCode1)')), [["3100"]]); 8 | //console.log(_.rql('placeName=string%3AHole%C5%A1ovice%2520%28%C4%8D%C3%A1st%29')); 9 | equal(_.query(_.clone(data), 'countryCode=TR&sort(-placeName)').length, 13); 10 | deepEqual(_.query(_.clone(data), 'countryCode=TR&sort(-placeName)&limit(2,2)&pick(placeName)'), 11 | [{placeName: 'Kizilcaören Köyü'},{placeName: 'Kemalköy Köyü'}]); 12 | }); 13 | } 14 | 15 | // equality of queries `a` and `b` 16 | function deq(a, b, msg){ 17 | //if (a && a.error && b && b.error) 18 | deepEqual(a.error, b.error, msg); 19 | //if (a && a.name && b && b.name) 20 | deepEqual(a.name, b.name, msg); 21 | if (_.isArray(a.args)) { 22 | for (var i = 0, l = a.args.length; i < l; ++i) { 23 | if (_.isObject(a.args[i])) { 24 | deq(a.args[i], b.args[i], msg); 25 | } else { 26 | deepEqual(a.args[i], b.args[i], msg); 27 | //deq(a.args[i], b.args[i], msg); 28 | } 29 | } 30 | } else { 31 | deepEqual(a.args, b.args, msg); 32 | //deq(a.args, b.args, msg); 33 | } 34 | } 35 | 36 | $(document).ready(function(){ 37 | 38 | /////////////////////////////////////// 39 | // 40 | // this data taken from kriszyp/rql tests 41 | // 42 | 43 | var queryPairs = { 44 | "arrays": [ 45 | {"a": {name:"and", args:["a"]}}, 46 | {"(a)": {name:"and", args:[["a"]]}}, 47 | {"(a=10)": {name:"and", args:[["a","10"]]}}, 48 | {"a,b,c": {name:"and", args:["a", "b", "c"]}}, 49 | {"(a,b,c)": {name:"and", args:[["a", "b", "c"]]}}, 50 | {"a(b)": {name:"and","args":[{"name":"a","args":["b"]}]}}, 51 | {"a(b,c)": {name:"and", args:[{name:"a", args:["b", "c"]}]}}, 52 | {"a((b),c)": {"name": "and", args:[{name:"a", args:[["b"], "c"]}]}}, 53 | {"a((b,c),d)": {name:"and", args:[{name:"a", args:[["b", "c"], "d"]}]}}, 54 | {"a(b/c,d)": {name:"and", args:[{name:"a", args:[["b", "c"], "d"]}]}}, 55 | {"a(b)&c(d(e))": {name:"and", args:[ 56 | {name:"a", args:["b"]}, 57 | {name:"c", args:[{name:"d", args:["e"]}]} 58 | ]}} 59 | ], 60 | "dot-comparison": [ 61 | {"foo.bar=3": {name:"and", args:[{name:"eq", args:["foo.bar",3]}]}}, 62 | {"select(sub.name)": {name:"and", args:[{name:"select", args:["sub.name"]}]}} 63 | ], 64 | "equality": [ 65 | {"eq(a,b)": {name:"and", args:[{name:"eq", args:["a","b"]}]}}, 66 | {"a=eq=b": "eq(a,b)"}, 67 | {"a=b": "eq(a,b)"} 68 | ], 69 | "inequality": [ 70 | {"ne(a,b)": {name:"and", args:[{name:"ne", args:["a", "b"]}]}}, 71 | {"a=ne=b": "ne(a,b)"}, 72 | {"a!=b": "ne(a,b)"} 73 | ], 74 | "less-than": [ 75 | {"lt(a,b)": {name:"and", args:[{name:"lt", args:["a", "b"]}]}}, 76 | {"a=lt=b": "lt(a,b)"}, 77 | {"ab": "gt(a,b)"} 88 | ], 89 | "greater-than-equal": [ 90 | {"ge(a,b)": {name:"and", args:[{name:"ge", args:["a", "b"]}]}}, 91 | {"a=ge=b": "ge(a,b)"}, 92 | {"a>=b": "ge(a,b)"} 93 | ], 94 | "nested comparisons": [ 95 | {"a(b(le(c,d)))": {name:"and", args:[{name:"a", args:[{name:"b", args:[{name:"le", args:["c", "d"]}]}]}]}}, 96 | {"a(b(c=le=d))": "a(b(le(c,d)))"}, 97 | {"a(b(c<=d))": "a(b(le(c,d)))"} 98 | ], 99 | "arbitrary FIQL desugaring": [ 100 | {"a=b=c": {name:"and", args:[{name:"b", args:["a", "c"]}]}}, 101 | {"a(b=cd=e)": {name:"and", args:[{name:"a", args:[{name:"cd", args:["b", "e"]}]}]}} 102 | ], 103 | "and grouping": [ 104 | {"a&b&c": {name:"and", args:["a", "b", "c"]}}, 105 | {"a(b)&c": {name:"and", args:[{name:"a", args:["b"]}, "c"]}}, 106 | {"a&(b&c)": {"name":"and","args":["a",{"name":"and","args":["b","c"]}]}} 107 | ], 108 | "or grouping": [ 109 | {"(a|b|c)": {name:"and", args:[{name:"or", args:["a", "b", "c"]}]}}, 110 | {"(a(b)|c)": {name:"and", args:[{name:"or", args:[{name:"a", args:["b"]}, "c"]}]}} 111 | ], 112 | "complex grouping": [ 113 | {"a&(b|c)": {name:"and", args:["a", {name:"or", args:["b", "c"]}]}}, 114 | {"a|(b&c)": {name:"and", args:[{name:"or", args:["a", {name:"and", args:["b", "c"]}]}]}}, 115 | {"a(b(c1000|val<1)').toMongo(), {meta: {sort: {val: -1}}, search: {$or: [{val: {$gt: 1000}}, {val: {$lt: 1}}]}}); 221 | }); 222 | 223 | test("binding parameters", function(){ 224 | var parsed; 225 | parsed = _.rql('in(id,$1)', [['a','b','c']]); 226 | deq(parsed, {name: 'and', args: [{name: 'in', args: ['id', ['a', 'b', 'c']]}]}); 227 | parsed = _.rql('eq(id,$1)', ['a']); 228 | deq(parsed, {name: 'and', args: [{name: 'eq', args: ['id', 'a']}]}); 229 | }); 230 | 231 | test("array of IDs", function(){ 232 | var parsed = _.rql(['a', ['b','c'], 'd']); 233 | //console.log('PIDS', parsed); 234 | deq(parsed, {name: 'and', args: [{name: 'in', args: ['id', ['a', ['b','c'], 'd']]}]}); 235 | }); 236 | 237 | test("stringification", function(){ 238 | var parsed; 239 | parsed = _.rql('eq(id1,RE:%5Eabc%5C%2F)'); 240 | // Hmmm. deepEqual gives null for regexps? 241 | ok(parsed.args[0].args[1].toString() === /^abc\//.toString()); 242 | //assert.deepEqual(parsed, {name: 'and', args: [{name: 'eq', args: ['id1', /^abc\//]}]}); 243 | ok(_.rql().eq('_1',/GGG(EE|FF)/i)+'' === 'eq(_1,re:GGG%28EE%7CFF%29)'); 244 | parsed = _.rql('eq(_1,re:GGG%28EE%7CFF%29)'); 245 | equal(parsed.args[0].args[1].toString(), /GGG(EE|FF)/i.toString()); 246 | //assert.ok(_.rql().eq('_1',/GGG(EE|FF)/)+'' === 'eq(_1,RE:GGG%28EE%7CFF%29)'); 247 | // string to array and back 248 | var str = 'somefunc(and(1),(a,b),(10,(10,1)),(a,b.c))'; 249 | equal(_.rql(str)+'', str); 250 | // quirky arguments 251 | var name = ['a/b','c.d']; 252 | equal(_.rql(_.rql().eq(name,1)+'')+'', 'eq((a%2Fb,c.d),1)'); 253 | deepEqual(_.rql(_.rql().eq(name,1)+'').args[0].args[0], name); 254 | // utf-8 255 | deepEqual(_.rql('placeName=a=Hole%C5%A1ovice%20%28%C4%8D%C3%A1st%29').args[0].args[1], 'Holešovice (část)', 'utf-8 conversion'); 256 | }); 257 | 258 | module("Array"); 259 | 260 | test("filtering #1", function(){ 261 | equal(_.query(data, "price<10").length, 1); 262 | equal(_.query(data, "price<11").length, 2); 263 | equal(_.query(data, "nested/property=value").length, 1); 264 | equal(_.query(data, "with%2Fslash=slashed").length, 1); 265 | equal(_.query(data, "out(price,(5,10))").length, 0); 266 | equal(_.query(data, "out(price,(5))").length, 1); 267 | equal(_.query(data, "contains(tags,even)").length, 1); 268 | equal(_.query(data, "contains(tags,fun)").length, 2); 269 | equal(_.query(data, "excludes(tags,fun)").length, 0); 270 | 271 | //console.log(_.query(data, "excludes(tags,ne(fun))"), _.query(null, "excludes(tags,ne(fun))")); 272 | // FIXME: failing! 273 | //equal(_.query(data, "excludes(tags,ne(fun))").length, 1); 274 | //equal(_.query(data, "excludes(tags,ne(even))").length, 0); 275 | 276 | deepEqual(_.query(data, "match(price,10)"), [data[0]]); 277 | deepEqual(_.query(data, "price=re:10"), [data[0]]); 278 | deepEqual(_.query(data, "price!=re:10"), [data[1]]); 279 | deepEqual(_.query(data, "match(name,f.*)"), [data[1]]); 280 | deepEqual(_.query(data, "match(name,glob:f*)"), [data[1]]); 281 | }); 282 | 283 | test("filtering #2", function(){ 284 | var data = [{ 285 | "path.1":[1,2,3] 286 | },{ 287 | "path.1":[9,3,7] 288 | }]; 289 | deepEqual(_.query(data, "contains(path,3)&sort()"), []); // path is undefined 290 | deepEqual(_.query(data, "contains(path.1,3)&sort(-path.1)"), [data[1], data[0]]); // 3 found in both 291 | deepEqual(_.query(data, "excludes(path.1,3)&sort()"), []); // 3 found in both 292 | deepEqual(_.query(data, "excludes(path.1,7)&sort()"), [data[0]]); // 7 found in second 293 | }); 294 | 295 | test("filtering #3", function(){ 296 | var data = [{ 297 | a:2,b:2,c:1,foo:{bar:'baz1',baz:'raz'} 298 | },{ 299 | a:1,b:4,c:1,foo:{bar:'baz2'} 300 | },{ 301 | a:3,b:0,c:1,foo:{bar:'baz3'} 302 | }]; 303 | deepEqual(_.query(data, ''), data, 'empty query'); 304 | deepEqual(_.query(data, 'a=2,b<4'), [data[0]], 'vanilla'); 305 | deepEqual(_.query(data, 'a=2,and(b<4)'), [data[0]], 'vanilla, extra and'); 306 | deepEqual(_.query(data, "a=2,b<4,pick(-b,a)"), [{a:2}], 'pick -/+'); 307 | deepEqual(_.query(data, 'or((pick(-b,a)&values(a/b/c)))'), [[],[],[]], 'pick -/+, values', 'fake or'); 308 | deepEqual(_.query(data, 'a>1,b<4,pick(b,foo/bar,-foo/baz,+fo.ba),limit(1,1)'), [{b:0,foo:{bar: 'baz3'}}], 'pick deep properties, limit'); 309 | deepEqual(_.query(data, 'or(eq(a,2),eq(b,4)),pick(b)'), [{b: 2}, {b: 4}], 'truly or'); 310 | deepEqual(_.query(data, 'and(and(and(hasOwnProperty!=%22123)))'), data, 'attempt to access prototype -- noop'); 311 | deepEqual(_.query(_.clone(data), 'sort(c,-foo/bar,foo/baz)'), [data[2], data[1], data[0]], 'sort'); 312 | deepEqual(_.query(data, 'match(foo/bar,z3)'), [data[2]], 'match'); 313 | deepEqual(_.query(data, 'foo/bar!=re:z3'), [data[0], data[1]], 'non-match'); 314 | deepEqual(_.query(data, 'foo/baz=re:z'), [data[0]], 'implicit match'); 315 | deepEqual(_.query(data, 'in(foo/bar,(baz1))'), [data[0]], 'occurance'); 316 | deepEqual(_.query(data, 'in(foo/bar,baz2)'), [data[1]], 'occurance in non-array'); 317 | deepEqual(_.query(data, 'nin(foo/bar,baz2)'), [data[0], data[2]], 'non-occurance in non-array'); 318 | deepEqual(_.query(data, 'between(foo/bar,baz1,baz3)'), [data[0], data[1]], 'between strings'); 319 | deepEqual(_.query(data, 'between(b,2,4)'), [data[0]], 'between numbers'); 320 | }); 321 | 322 | }); 323 | -------------------------------------------------------------------------------- /test/schema.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | 3 | // 4 | //////////////////////////////////////////////////////////////////// 5 | // 6 | 7 | (function(){ 8 | var obj; 9 | 10 | module('Validate: coerce'); 11 | 12 | test('falsy', function(){ 13 | equal(_.coerce(undefined, 'string'), ''); 14 | equal(_.coerce(null, 'string'), ''); 15 | equal(_.coerce(0, 'string'), '0'); 16 | equal(_.coerce(false, 'string'), 'false'); 17 | equal(_.coerce(NaN, 'string'), 'NaN'); 18 | }); 19 | 20 | test('truthy', function(){ 21 | equal(_.coerce(1, 'string'), '1'); 22 | equal(_.coerce(1.0, 'string'), '1'); 23 | equal(_.coerce(1.1, 'string'), '1.1'); 24 | equal(_.coerce(true, 'string'), 'true'); 25 | equal(_.coerce([], 'string'), ''); 26 | equal(_.coerce({}, 'string'), '[object Object]'); 27 | }); 28 | 29 | })(); 30 | 31 | // 32 | //////////////////////////////////////////////////////////////////// 33 | // 34 | 35 | (function(){ 36 | var obj; 37 | 38 | module('Validate: schema'); 39 | 40 | test('value attribute', function(){ 41 | obj = {id: 'bac', foo: '4'}; 42 | equal(_.validate(obj, { 43 | type: 'object', 44 | properties: { 45 | foo: { 46 | value: 'Зафиксировано' 47 | } 48 | }, 49 | additionalProperties: true 50 | }, {veto: true, removeAdditionalProps: false, flavor: 'add', coerce: true}), 51 | null, 'coerced and added ok'); 52 | deepEqual(obj, {id: 'bac', foo: 'Зафиксировано'}, 'schema ok'); 53 | }); 54 | 55 | test('empty properties', function(){ 56 | obj = {id: 'bac', foo: '4', bar: 'vareniki', spam: true}; 57 | equal(_.validate(obj, { 58 | type: 'object', 59 | properties: { 60 | }, 61 | additionalProperties: true 62 | }, {veto: true, removeAdditionalProps: false, flavor: 'add', coerce: true}), 63 | null, 'coerced and added ok'); 64 | deepEqual(obj, {id: 'bac', foo: '4', bar: 'vareniki', spam: true}, 'schema ok'); 65 | }); 66 | 67 | test('undefined properties', function(){ 68 | obj = {id: 'bac', foo: '4', bar: 'vareniki', spam: true}; 69 | equal(_.validate(obj, { 70 | type: 'object', 71 | additionalProperties: true 72 | }, {veto: true, removeAdditionalProps: false, flavor: 'add', coerce: true}), 73 | null, 'coerced and added ok'); 74 | deepEqual(obj, {id: 'bac', foo: '4', bar: 'vareniki', spam: true}, 'schema ok'); 75 | }); 76 | 77 | test('undefined properties and additionalProperties=false', function(){ 78 | obj = {id: 'bac', foo: '4', bar: 'vareniki', spam: true}; 79 | equal(_.validate(obj, { 80 | type: 'object', 81 | additionalProperties: false 82 | }, {veto: true, removeAdditionalProps: true, flavor: 'add', coerce: true}), 83 | null, 'coerced and added ok'); 84 | deepEqual(obj, {id: 'bac', foo: '4', bar: 'vareniki', spam: true}, 'schema ok'); 85 | }); 86 | 87 | test('greedy coercion for optionals', function(){ 88 | obj = {foo: undefined, bar: null}; 89 | equal(_.validate(obj, { 90 | type: 'object', 91 | properties: { 92 | foo: { 93 | type: 'string', 94 | pattern: /^aaa$/, 95 | optional: true 96 | }, 97 | bar: { 98 | } 99 | }, 100 | additionalProperties: true 101 | }, {veto: true, removeAdditionalProps: false, flavor: 'add', coerce: true}), 102 | null, 'optionals not coerced ok'); 103 | deepEqual(obj, {bar: null}, 'schema ok'); 104 | }); 105 | 106 | test('fixed values', function(){ 107 | obj = {foo: 'baz'}; 108 | equal(_.validate(obj, { 109 | type: 'object', 110 | properties: { 111 | foo: { 112 | type: 'string', 113 | value: 'bar', 114 | veto: { 115 | update: true 116 | } 117 | } 118 | }, 119 | additionalProperties: false 120 | }, {veto: true, removeAdditionalProps: true, flavor: 'add', coerce: true}), 121 | null, 'fixed values fixed ok'); 122 | deepEqual(obj, {foo: 'bar'}, 'schema ok'); 123 | // 124 | obj = {}; 125 | equal(_.validate(obj, { 126 | type: 'object', 127 | properties: { 128 | foo: { 129 | type: 'string', 130 | value: 'bar', 131 | veto: { 132 | update: true 133 | } 134 | } 135 | }, 136 | additionalProperties: false 137 | }, {veto: true, removeAdditionalProps: true, flavor: 'add', coerce: true}), 138 | null, 'fixed values fixed ok'); 139 | deepEqual(obj, {foo: 'bar'}, 'schema ok'); 140 | }); 141 | 142 | })(); 143 | 144 | // 145 | //////////////////////////////////////////////////////////////////// 146 | // 147 | 148 | (function(){ 149 | var schema, obj; 150 | 151 | module('Validate: additionalProperties=false'); 152 | 153 | schema = { 154 | type: 'object', 155 | properties: { 156 | id: { 157 | type: 'string', 158 | pattern: /^[abc]+$/, 159 | veto: { 160 | update: true 161 | } 162 | }, 163 | foo: { 164 | type: 'integer', 165 | veto: { 166 | get: true 167 | } 168 | }, 169 | bar: { 170 | type: 'array', 171 | items: { 172 | type: 'string', 173 | 'enum': ['eniki', 'beniki', 'eli', 'vareniki'] 174 | }, 175 | veto: { 176 | query: true 177 | } 178 | }, 179 | defaulty: { 180 | type: 'date', 181 | 'default': '2011-02-14' 182 | } 183 | }, 184 | additionalProperties: false 185 | }; 186 | 187 | test('add', function(){ 188 | obj = {id: 'bac', foo: '4', bar: 'vareniki', spam: true}; 189 | equal(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 190 | null, 'coerced and added ok'); 191 | //console.log(obj.defaulty, _.parseDate('2011-02-14')); 192 | deepEqual(obj, {id: 'bac', foo: 4, bar: ['vareniki'], defaulty: _.parseDate('2011-02-14')}, 'coerced for "add" ok'); 193 | // 194 | obj = {id: 'bac1', foo: 'a', bar: 'pelmeshki'}; 195 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 196 | [{property: 'id', message: 'pattern'}, {'property': 'foo', 'message': 'type'}, {'property': 'bar[0]', 'message': 'enum'}], 'validate for "add"'); 197 | // 198 | obj = {id: 'bac'}; 199 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 200 | [{'property': 'foo', 'message': 'required'}, {'property': 'bar', 'message': 'required'}], 'validate for "add"'); 201 | }); 202 | 203 | test('update', function(){ 204 | obj = {id: 'bac', foo1: '5', bar: ['eli', 'eniki']}; 205 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 206 | null, 'validate for "update" nak: required'); 207 | deepEqual(obj, {bar: ['eli', 'eniki']}, 'validate for "update" ok'); 208 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki']}; 209 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 210 | null, 'validate for "update" ok'); 211 | deepEqual(obj, {foo: 5, bar: ['eli', 'eniki']}, 'validate for "update" ok'); 212 | }); 213 | 214 | test('get', function(){ 215 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki'], secret: true}; 216 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'get'}), 217 | null, 'validate for "get" ok'); 218 | deepEqual(obj, {id: 'bac', bar: ['eli', 'eniki']}, 'validate for "get" ok'); 219 | }); 220 | 221 | test('query', function(){ 222 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki'], secret: true}; 223 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'query'}), 224 | null, 'validate for "query" ok'); 225 | deepEqual(obj, {id: 'bac', foo: '5'}, 'validate for "query" ok'); 226 | }); 227 | 228 | })(); 229 | 230 | // 231 | //////////////////////////////////////////////////////////////////// 232 | // 233 | 234 | (function(){ 235 | var schema, obj; 236 | 237 | module('Validate: additionalProperties=true'); 238 | 239 | schema = { 240 | type: 'object', 241 | properties: { 242 | id: { 243 | type: 'string', 244 | pattern: /^[abc]+$/, 245 | veto: { 246 | update: true 247 | } 248 | }, 249 | foo: { 250 | type: 'integer', 251 | veto: { 252 | get: true 253 | } 254 | }, 255 | bar: { 256 | type: 'array', 257 | items: { 258 | type: 'string', 259 | 'enum': ['eniki', 'beniki', 'eli', 'vareniki'] 260 | }, 261 | veto: { 262 | query: true 263 | } 264 | }, 265 | defaulty: { 266 | type: 'date', 267 | 'default': '2011-02-14' 268 | } 269 | }, 270 | additionalProperties: true 271 | }; 272 | 273 | test('add', function(){ 274 | obj = {id: 'bac', foo: '4', bar: 'vareniki', spam: true}; 275 | equal(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 276 | null, 'coerced and added ok'); 277 | deepEqual(obj, {id: 'bac', foo: 4, bar: ['vareniki'], defaulty: _.parseDate('2011-02-14'), spam: true}, 'coerced for "add" ok'); 278 | obj = {id: 'bac1', foo: 'a', bar: 'pelmeshki'}; 279 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 280 | [{property: 'id', message: 'pattern'}, {'property': 'foo', 'message': 'type'}, {'property': 'bar[0]', 'message': 'enum'}], 'validate for "add"'); 281 | }); 282 | 283 | test('update', function(){ 284 | obj = {id: 'bac', foo1: '5', bar: ['eli', 'eniki']}; 285 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 286 | null, 'validate for "update" nak: required'); 287 | deepEqual(obj, {bar: ['eli', 'eniki'], foo1: '5'}, 'validate for "update" ok'); 288 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki']}; 289 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 290 | null, 'validate for "update" ok'); 291 | deepEqual(obj, {foo: 5, bar: ['eli', 'eniki']}, 'validate for "update" ok'); 292 | }); 293 | 294 | test('get', function(){ 295 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki'], secret: true}; 296 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'get'}), 297 | null, 'validate for "get" ok'); 298 | deepEqual(obj, {id: 'bac', bar: ['eli', 'eniki'], secret: true}, 'validate for "get" ok'); 299 | }); 300 | 301 | test('query', function(){ 302 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki'], secret: true}; 303 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'query'}), 304 | null, 'validate for "query" ok'); 305 | deepEqual(obj, {id: 'bac', foo: '5', secret: true}, 'validate for "query" ok'); 306 | }); 307 | 308 | })(); 309 | 310 | // 311 | //////////////////////////////////////////////////////////////////// 312 | // 313 | 314 | (function(){ 315 | var schema, obj; 316 | 317 | module('Validate: additionalProperties=schema'); 318 | 319 | schema = { 320 | type: 'object', 321 | properties: { 322 | id: { 323 | type: 'string', 324 | pattern: /^[abc]+$/, 325 | veto: { 326 | update: true 327 | } 328 | }, 329 | foo: { 330 | type: 'integer', 331 | veto: { 332 | get: true 333 | } 334 | }, 335 | bar: { 336 | type: 'array', 337 | items: { 338 | type: 'string', 339 | 'enum': ['eniki', 'beniki', 'eli', 'vareniki'] 340 | } 341 | , 342 | veto: { 343 | query: true 344 | } 345 | }, 346 | defaulty: { 347 | type: 'date', 348 | 'default': '2011-12-31' 349 | } 350 | }, 351 | additionalProperties: { 352 | type: 'number', 353 | maxDecimal: 2 354 | } 355 | }; 356 | 357 | test('add', function(){ 358 | obj = {id: 'bac', foo: '4', bar: 'vareniki', spam: true}; 359 | equal(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 360 | null, 'coerced and added ok'); 361 | deepEqual(obj, {id: 'bac', foo: 4, bar: ['vareniki'], defaulty: _.parseDate('2011-12-31'), spam: 1}, 'coerced for "add" ok'); 362 | obj = {id: 'bac1', foo: 'a', bar: 'pelmeshki'}; 363 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'add', coerce: true}), 364 | [{property: 'id', message: 'pattern'}, {'property': 'foo', 'message': 'type'}, {'property': 'bar[0]', 'message': 'enum'}], 'validate for "add"'); 365 | }); 366 | 367 | test('update', function(){ 368 | obj = {id: 'bac', foo1: '5.111', bar: ['eli', 'eniki']}; 369 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 370 | [{property: 'foo1', message: 'digits'}], 'validate for "update" nak: digits'); 371 | obj = {id: 'bac', foo1: '5.11', bar: ['eli', 'eniki']}; 372 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 373 | null, 'validate for "update" ok'); 374 | deepEqual(obj, {bar: ['eli', 'eniki'], foo1: 5.11}, 'validate for "update" ok'); 375 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki']}; 376 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, existingOnly: true, flavor: 'update', coerce: true}), 377 | null, 'validate for "update" ok'); 378 | deepEqual(obj, {foo: 5, bar: ['eli', 'eniki']}, 'validate for "update" ok'); 379 | }); 380 | 381 | test('get', function(){ 382 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki'], secret: true}; 383 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'get'}), 384 | null, 'validate for "get" ok'); 385 | deepEqual(obj, {id: 'bac', bar: ['eli', 'eniki'], secret: true}, 'validate for "get" ok'); 386 | }); 387 | 388 | test('query', function(){ 389 | obj = {id: 'bac', foo: '5', bar: ['eli', 'eniki'], secret: true}; 390 | deepEqual(_.validate(obj, schema, {veto: true, removeAdditionalProps: !schema.additionalProperties, flavor: 'query'}), 391 | null, 'validate for "query" ok'); 392 | deepEqual(obj, {id: 'bac', foo: '5', secret: true}, 'validate for "query" ok'); 393 | }); 394 | 395 | })(); 396 | 397 | // 398 | //////////////////////////////////////////////////////////////////// 399 | // 400 | 401 | }); 402 | -------------------------------------------------------------------------------- /test/webify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /*! 6 | * 7 | * 8 | * A simple drop-in to make the current directory available 9 | * via http://*:8080 10 | * 11 | * 12 | * Copyright (c) 2011 Vladimir Dronnikov (dronnikov@gmail.com) 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | * 33 | */ 34 | 35 | var Fs = require('fs'); 36 | var parseUrl = require('url').parse; 37 | 38 | var pwd = process.cwd(); 39 | var ENOENT = require('constants').ENOENT; 40 | 41 | function mime(url) { 42 | if (url.slice(-5) === '.html') return 'text/html'; 43 | if (url.slice(-4) === '.css') return 'text/css'; 44 | if (url.slice(-3) === '.js') return 'text/javascript'; 45 | return 'text/plain'; 46 | } 47 | 48 | require('http').createServer(function(req, res) { 49 | req.uri = parseUrl(req.url); 50 | if (req.uri.pathname === '/') req.uri.pathname = '/index.html'; 51 | Fs.readFile(pwd + req.uri.pathname, function(err, data) { 52 | if (err) { 53 | if (err.errno === ENOENT) { 54 | res.writeHead(404); 55 | res.end(); 56 | } else { 57 | res.writeHead(500); 58 | res.end(err.stack); 59 | } 60 | } else { 61 | res.writeHead(200, { 62 | 'Content-Type': mime(req.uri.pathname), 63 | 'Content-Length': data.length 64 | }); 65 | res.end(data); 66 | } 67 | }); 68 | }).listen(8080); 69 | console.log('Listening to http://*:8080. Use Ctrl+C to stop.'); 70 | -------------------------------------------------------------------------------- /underscore-data.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (this._ === void 0 && this.$ !== void 0 && $.ender) { 3 | this._ = $; 4 | this._.mixin = this.$.ender; 5 | } 6 | }).call(this); 7 | (function() { 8 | var __slice = Array.prototype.slice; 9 | if (this._ === void 0 && this.$ !== void 0 && $.ender) { 10 | this._ = $; 11 | this._.mixin = this.$.ender; 12 | } 13 | 'use strict'; 14 | /* 15 | * 16 | * Copyright(c) 2011 Vladimir Dronnikov 17 | * MIT Licensed 18 | * 19 | */ 20 | _.mixin({ 21 | isObject: function(value) { 22 | return value && typeof value === 'object'; 23 | }, 24 | ensureArray: function(value) { 25 | if (!value) { 26 | if (value === void 0) { 27 | return []; 28 | } else { 29 | return [value]; 30 | } 31 | } 32 | if (_.isString(value)) { 33 | return [value]; 34 | } 35 | return _.toArray(value); 36 | }, 37 | toHash: function(list, field) { 38 | var r; 39 | r = {}; 40 | _.each(list, function(x) { 41 | var f; 42 | f = _.drill(x, field); 43 | return r[f] = x; 44 | }); 45 | return r; 46 | }, 47 | freeze: function(obj) { 48 | if (_.isObject(obj)) { 49 | Object.freeze(obj); 50 | _.each(obj, function(v, k) { 51 | return _.freeze(v); 52 | }); 53 | } 54 | return obj; 55 | }, 56 | proxy: function(obj, exposes) { 57 | var facet; 58 | facet = {}; 59 | _.each(exposes, function(definition) { 60 | var name, prop; 61 | if (_.isArray(definition)) { 62 | name = definition[1]; 63 | prop = definition[0]; 64 | if (!_.isFunction(prop)) { 65 | prop = _.drill(obj, prop); 66 | } 67 | } else { 68 | name = definition; 69 | prop = obj[name]; 70 | } 71 | if (prop) { 72 | return facet[name] = prop; 73 | } 74 | }); 75 | return Object.freeze(facet); 76 | }, 77 | drill: function(obj, path, remove) { 78 | var index, name, orig, part, _i, _j, _len, _len2, _ref; 79 | if (_.isArray(path)) { 80 | if (remove) { 81 | _ref = path, path = 2 <= _ref.length ? __slice.call(_ref, 0, _i = _ref.length - 1) : (_i = 0, []), name = _ref[_i++]; 82 | orig = obj; 83 | for (index = 0, _len = path.length; index < _len; index++) { 84 | part = path[index]; 85 | obj = obj && obj[part]; 86 | } 87 | if (obj != null ? obj[name] : void 0) { 88 | delete obj[name]; 89 | } 90 | return orig; 91 | } else { 92 | for (_j = 0, _len2 = path.length; _j < _len2; _j++) { 93 | part = path[_j]; 94 | obj = obj && obj[part]; 95 | } 96 | return obj; 97 | } 98 | } else if (path === void 0) { 99 | return obj; 100 | } else { 101 | if (remove) { 102 | delete obj[path]; 103 | return obj; 104 | } else { 105 | return obj[path]; 106 | } 107 | } 108 | } 109 | }); 110 | _.mixin({ 111 | parseDate: function(value) { 112 | var date, parts; 113 | date = new Date(value); 114 | if (_.isDate(date)) { 115 | return date; 116 | } 117 | parts = String(value).match(/(\d+)/g); 118 | return new Date(parts[0], (parts[1] || 1) - 1, parts[2] || 1); 119 | }, 120 | isDate: function(obj) { 121 | return !!((obj != null ? obj.getTimezoneOffset : void 0) && obj.setUTCFullYear && !_.isNaN(obj.getTime())); 122 | } 123 | }); 124 | }).call(this); 125 | (function() { 126 | var coerce, validate; 127 | var __slice = Array.prototype.slice, __hasProp = Object.prototype.hasOwnProperty; 128 | if (this._ === void 0 && this.$ !== void 0 && $.ender) { 129 | this._ = $; 130 | this._.mixin = this.$.ender; 131 | } 132 | 'use strict'; 133 | /* 134 | * 135 | * Copyright(c) 2011 Vladimir Dronnikov 136 | * MIT Licensed 137 | * 138 | */ 139 | _.mixin({ 140 | isObject: function(value) { 141 | return value && typeof value === 'object'; 142 | }, 143 | ensureArray: function(value) { 144 | if (!value) { 145 | if (value === void 0) { 146 | return []; 147 | } else { 148 | return [value]; 149 | } 150 | } 151 | if (_.isString(value)) { 152 | return [value]; 153 | } 154 | return _.toArray(value); 155 | }, 156 | toHash: function(list, field) { 157 | var r; 158 | r = {}; 159 | _.each(list, function(x) { 160 | var f; 161 | f = _.drill(x, field); 162 | return r[f] = x; 163 | }); 164 | return r; 165 | }, 166 | freeze: function(obj) { 167 | if (_.isObject(obj)) { 168 | Object.freeze(obj); 169 | _.each(obj, function(v, k) { 170 | return _.freeze(v); 171 | }); 172 | } 173 | return obj; 174 | }, 175 | proxy: function(obj, exposes) { 176 | var facet; 177 | facet = {}; 178 | _.each(exposes, function(definition) { 179 | var name, prop; 180 | if (_.isArray(definition)) { 181 | name = definition[1]; 182 | prop = definition[0]; 183 | if (!_.isFunction(prop)) { 184 | prop = _.drill(obj, prop); 185 | } 186 | } else { 187 | name = definition; 188 | prop = obj[name]; 189 | } 190 | if (prop) { 191 | return facet[name] = prop; 192 | } 193 | }); 194 | return Object.freeze(facet); 195 | }, 196 | drill: function(obj, path, remove) { 197 | var index, name, orig, part, _i, _j, _len, _len2, _ref; 198 | if (_.isArray(path)) { 199 | if (remove) { 200 | _ref = path, path = 2 <= _ref.length ? __slice.call(_ref, 0, _i = _ref.length - 1) : (_i = 0, []), name = _ref[_i++]; 201 | orig = obj; 202 | for (index = 0, _len = path.length; index < _len; index++) { 203 | part = path[index]; 204 | obj = obj && obj[part]; 205 | } 206 | if (obj != null ? obj[name] : void 0) { 207 | delete obj[name]; 208 | } 209 | return orig; 210 | } else { 211 | for (_j = 0, _len2 = path.length; _j < _len2; _j++) { 212 | part = path[_j]; 213 | obj = obj && obj[part]; 214 | } 215 | return obj; 216 | } 217 | } else if (path === void 0) { 218 | return obj; 219 | } else { 220 | if (remove) { 221 | delete obj[path]; 222 | return obj; 223 | } else { 224 | return obj[path]; 225 | } 226 | } 227 | } 228 | }); 229 | _.mixin({ 230 | parseDate: function(value) { 231 | var date, parts; 232 | date = new Date(value); 233 | if (_.isDate(date)) { 234 | return date; 235 | } 236 | parts = String(value).match(/(\d+)/g); 237 | return new Date(parts[0], (parts[1] || 1) - 1, parts[2] || 1); 238 | }, 239 | isDate: function(obj) { 240 | return !!((obj != null ? obj.getTimezoneOffset : void 0) && obj.setUTCFullYear && !_.isNaN(obj.getTime())); 241 | } 242 | }); 243 | 'use strict'; 244 | /* 245 | 246 | JSONSchema Validator - Validates JavaScript objects using JSON Schemas 247 | (http://www.json.com/json-schema-proposal/) 248 | 249 | Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com) 250 | Copyright (c) 2011 Vladimir Dronnikov dronnikov@gmail.com 251 | 252 | Licensed under the MIT (MIT-LICENSE.txt) license 253 | 254 | */ 255 | /* 256 | * 257 | * Copyright(c) 2011 Vladimir Dronnikov 258 | * MIT Licensed 259 | * 260 | */ 261 | /* 262 | Rewrite of kriszyp's json-schema validator https://github.com/kriszyp/json-schema 263 | Relies on documentcloud/underscore to normalize JS 264 | */ 265 | coerce = function(value, type) { 266 | var date; 267 | if (type === 'string') { 268 | value = value != null ? String(value) : ''; 269 | } else if (type === 'number' || type === 'integer') { 270 | if (!_.isNaN(value)) { 271 | value = Number(value); 272 | if (type === 'integer') { 273 | value = Math.floor(value); 274 | } 275 | } 276 | } else if (type === 'boolean') { 277 | value = value === 'false' ? false : !!value; 278 | } else if (type === 'null') { 279 | value = null; 280 | } else if (type === 'object') { 281 | if (typeof JSON !== "undefined" && JSON !== null ? JSON.parse : void 0) { 282 | try { 283 | value = JSON.parse(value); 284 | } catch (err) { 285 | 286 | } 287 | } 288 | } else if (type === 'array') { 289 | value = _.ensureArray(value); 290 | } else if (type === 'date') { 291 | date = _.parseDate(value); 292 | if (_.isDate(date)) { 293 | value = date; 294 | } 295 | } 296 | return value; 297 | }; 298 | validate = function(instance, schema, options, callback) { 299 | var async, asyncs, checkObj, checkProp, errors, i, len, self, _changing, _fn, _len; 300 | if (options == null) { 301 | options = {}; 302 | } 303 | self = this; 304 | _changing = options.changing; 305 | asyncs = []; 306 | errors = []; 307 | checkProp = function(value, schema, path, i) { 308 | var addError, checkType, enumeration, itemsIsArray, propDef, v, _len; 309 | if (path) { 310 | if (_.isNumber(i)) { 311 | path += '[' + i + ']'; 312 | } else if (i === void 0) { 313 | path += ''; 314 | } else { 315 | path += '.' + i; 316 | } 317 | } else { 318 | path += i; 319 | } 320 | addError = function(message) { 321 | return errors.push({ 322 | property: path, 323 | message: message 324 | }); 325 | }; 326 | if ((typeof schema !== 'object' || _.isArray(schema)) && (path || typeof schema !== 'function') && !(schema != null ? schema.type : void 0)) { 327 | if (_.isFunction(schema)) { 328 | if (!(value instanceof schema)) { 329 | addError('type'); 330 | } 331 | } else if (schema) { 332 | addError('invalid'); 333 | } 334 | return null; 335 | } 336 | if (_changing && schema.readonly) { 337 | addError('readonly'); 338 | } 339 | if (schema["extends"]) { 340 | checkProp(value, schema["extends"], path, i); 341 | } 342 | checkType = function(type, value) { 343 | var priorErrors, t, theseErrors, unionErrors, _i, _len; 344 | if (type) { 345 | if (typeof type === 'string' && type !== 'any' && (type == 'null' ? value !== null : typeof value !== type) && 346 | !(type === 'array' && _.isArray(value)) && 347 | !(type === 'date' && _.isDate(value)) && 348 | !(type === 'integer' && value%1===0)) { 349 | return [ 350 | { 351 | property: path, 352 | message: 'type' 353 | } 354 | ]; 355 | } 356 | if (_.isArray(type)) { 357 | unionErrors = []; 358 | for (_i = 0, _len = type.length; _i < _len; _i++) { 359 | t = type[_i]; 360 | unionErrors = checkType(t, value); 361 | if (!unionErrors.length) { 362 | break; 363 | } 364 | } 365 | if (unionErrors.length) { 366 | return unionErrors; 367 | } 368 | } else if (typeof type === 'object') { 369 | priorErrors = errors; 370 | errors = []; 371 | checkProp(value, type, path); 372 | theseErrors = errors; 373 | errors = priorErrors; 374 | return theseErrors; 375 | } 376 | } 377 | return []; 378 | }; 379 | if (value === void 0) { 380 | if ((!schema.optional || typeof schema.optional === 'object' && !schema.optional[options.flavor]) && !schema.get && !(schema["default"] != null)) { 381 | addError('required'); 382 | } 383 | } else { 384 | errors = errors.concat(checkType(schema.type, value)); 385 | if (schema.disallow && !checkType(schema.disallow, value).length) { 386 | addError('disallowed'); 387 | } 388 | if (value !== null) { 389 | if (_.isArray(value)) { 390 | if (schema.items) { 391 | itemsIsArray = _.isArray(schema.items); 392 | propDef = schema.items; 393 | for (i = 0, _len = value.length; i < _len; i++) { 394 | v = value[i]; 395 | if (itemsIsArray) { 396 | propDef = schema.items[i]; 397 | } 398 | if (options.coerce && propDef.type) { 399 | value[i] = coerce(v, propDef.type); 400 | } 401 | errors.concat(checkProp(v, propDef, path, i)); 402 | } 403 | } 404 | if (schema.minItems && value.length < schema.minItems) { 405 | addError('minItems'); 406 | } 407 | if (schema.maxItems && value.length > schema.maxItems) { 408 | addError('maxItems'); 409 | } 410 | } else if (schema.properties || schema.additionalProperties) { 411 | errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties)); 412 | } 413 | if (_.isString(value)) { 414 | if (schema.pattern && !value.match(schema.pattern)) { 415 | addError('pattern'); 416 | } 417 | if (schema.maxLength && value.length > schema.maxLength) { 418 | addError('maxLength'); 419 | } 420 | if (schema.minLength && value.length < schema.minLength) { 421 | addError('minLength'); 422 | } 423 | } 424 | if (schema.minimum !== void 0 && typeof value === typeof schema.minimum && schema.minimum > value) { 425 | addError('minimum'); 426 | } 427 | if (schema.maximum !== void 0 && typeof value === typeof schema.maximum && schema.maximum < value) { 428 | addError('maximum'); 429 | } 430 | if (schema["enum"]) { 431 | enumeration = schema["enum"]; 432 | if (_.isFunction(enumeration)) { 433 | if (enumeration.length === 2) { 434 | asyncs.push({ 435 | value: value, 436 | path: path, 437 | fetch: enumeration 438 | }); 439 | } else if (enumeration.length === 1) { 440 | if (!enumeration.call(self, value)) { 441 | addError('enum'); 442 | } 443 | } else { 444 | enumeration = enumeration.call(self); 445 | if (!_.include(enumeration, value)) { 446 | addError('enum'); 447 | } 448 | } 449 | } else { 450 | if (!_.include(enumeration, value)) { 451 | addError('enum'); 452 | } 453 | } 454 | } 455 | if (_.isNumber(schema.maxDecimal) && (new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")).test(value)) { 456 | addError('digits'); 457 | } 458 | } 459 | } 460 | return null; 461 | }; 462 | checkObj = function(instance, objTypeDef, path, additionalProp) { 463 | var i, propDef, requires, value, _ref, _ref2, _ref3; 464 | if (objTypeDef == null) { 465 | objTypeDef = {}; 466 | } 467 | if (_.isObject(objTypeDef)) { 468 | if (typeof instance !== 'object' || _.isArray(instance)) { 469 | errors.push({ 470 | property: path, 471 | message: 'type' 472 | }); 473 | } 474 | for (i in objTypeDef) { 475 | if (!__hasProp.call(objTypeDef, i)) continue; 476 | propDef = objTypeDef[i]; 477 | value = instance[i]; 478 | if ('value' in propDef && ((_ref = options.flavor) === 'add' || _ref === 'update')) { 479 | value = instance[i] = propDef.value; 480 | } 481 | if (value === void 0 && options.existingOnly) { 482 | continue; 483 | } 484 | if (options.veto && (propDef.veto === true || typeof propDef.veto === 'object' && propDef.veto[options.flavor])) { 485 | delete instance[i]; 486 | continue; 487 | } 488 | if (((_ref2 = options.flavor) === 'query' || _ref2 === 'get') && !options.coerce) { 489 | continue; 490 | } 491 | if (value === void 0 && (propDef["default"] != null) && options.flavor === 'add') { 492 | value = instance[i] = propDef["default"]; 493 | } 494 | if (value === void 0 && options.flavor !== 'add') { 495 | delete instance[i]; 496 | continue; 497 | } 498 | if (options.coerce && propDef.type && i in instance && value !== void 0) { 499 | value = coerce(value, propDef.type); 500 | instance[i] = value; 501 | } 502 | if (value === void 0 && propDef.optional) { 503 | delete instance[i]; 504 | continue; 505 | } 506 | checkProp(value, propDef, path, i); 507 | } 508 | } 509 | for (i in instance) { 510 | value = instance[i]; 511 | if (i in instance && !objTypeDef[i] && (additionalProp === false || options.removeAdditionalProps)) { 512 | if (options.removeAdditionalProps) { 513 | delete instance[i]; 514 | continue; 515 | } else { 516 | errors.push({ 517 | property: path, 518 | message: 'unspecifed' 519 | }); 520 | } 521 | } 522 | requires = (_ref3 = objTypeDef[i]) != null ? _ref3.requires : void 0; 523 | if (requires && !requires in instance) { 524 | errors.push({ 525 | property: path, 526 | message: 'requires' 527 | }); 528 | } 529 | if ((additionalProp != null ? additionalProp.type : void 0) && !objTypeDef[i]) { 530 | if (options.coerce && additionalProp.type) { 531 | value = coerce(value, additionalProp.type); 532 | instance[i] = value; 533 | checkProp(value, additionalProp, path, i); 534 | } 535 | } 536 | if (!_changing && (value != null ? value.$schema : void 0)) { 537 | errors = errors.concat(checkProp(value, value.$schema, path, i)); 538 | } 539 | } 540 | return errors; 541 | }; 542 | if (schema) { 543 | checkProp(instance, schema, '', _changing || ''); 544 | } 545 | if (!_changing && (instance != null ? instance.$schema : void 0)) { 546 | checkProp(instance, instance.$schema, '', ''); 547 | } 548 | len = asyncs.length; 549 | if (callback && len) { 550 | _fn = function(async) { 551 | return async.fetch.call(self, async.value, function(err) { 552 | if (err) { 553 | errors.push({ 554 | property: async.path, 555 | message: 'enum' 556 | }); 557 | } 558 | len -= 1; 559 | if (!len) { 560 | return callback(errors.length && errors || null, instance); 561 | } 562 | }); 563 | }; 564 | for (i = 0, _len = asyncs.length; i < _len; i++) { 565 | async = asyncs[i]; 566 | _fn(async); 567 | } 568 | } else if (callback) { 569 | callback(errors.length && errors || null, instance); 570 | } else { 571 | return errors.length && errors || null; 572 | } 573 | }; 574 | _.mixin({ 575 | coerce: coerce, 576 | validate: validate 577 | }); 578 | }).call(this); 579 | (function() { 580 | var Query, autoConverted, coerce, converters, encodeString, encodeValue, jsOperatorMap, operatorMap, operators, parse, plusMinus, query, queryToString, requires_array, stringToValue, stringify, valid_funcs, valid_operators, validate; 581 | var __slice = Array.prototype.slice, __hasProp = Object.prototype.hasOwnProperty; 582 | if (this._ === void 0 && this.$ !== void 0 && $.ender) { 583 | this._ = $; 584 | this._.mixin = this.$.ender; 585 | } 586 | 'use strict'; 587 | /* 588 | * 589 | * Copyright(c) 2011 Vladimir Dronnikov 590 | * MIT Licensed 591 | * 592 | */ 593 | _.mixin({ 594 | isObject: function(value) { 595 | return value && typeof value === 'object'; 596 | }, 597 | ensureArray: function(value) { 598 | if (!value) { 599 | if (value === void 0) { 600 | return []; 601 | } else { 602 | return [value]; 603 | } 604 | } 605 | if (_.isString(value)) { 606 | return [value]; 607 | } 608 | return _.toArray(value); 609 | }, 610 | toHash: function(list, field) { 611 | var r; 612 | r = {}; 613 | _.each(list, function(x) { 614 | var f; 615 | f = _.drill(x, field); 616 | return r[f] = x; 617 | }); 618 | return r; 619 | }, 620 | freeze: function(obj) { 621 | if (_.isObject(obj) || _.isArray(obj)) { 622 | Object.freeze(obj); 623 | _.each(obj, function(v, k) { 624 | return _.freeze(v); 625 | }); 626 | } 627 | return obj; 628 | }, 629 | proxy: function(obj, exposes) { 630 | var facet; 631 | facet = {}; 632 | _.each(exposes, function(definition) { 633 | var name, prop; 634 | if (_.isArray(definition)) { 635 | name = definition[1]; 636 | prop = definition[0]; 637 | if (!_.isFunction(prop)) { 638 | prop = _.drill(obj, prop); 639 | } 640 | } else { 641 | name = definition; 642 | prop = obj[name]; 643 | } 644 | if (prop) { 645 | return facet[name] = prop; 646 | } 647 | }); 648 | return Object.freeze(facet); 649 | }, 650 | drill: function(obj, path, remove) { 651 | var index, name, orig, part, _i, _j, _len, _len2, _ref; 652 | if (_.isArray(path)) { 653 | if (remove) { 654 | _ref = path, path = 2 <= _ref.length ? __slice.call(_ref, 0, _i = _ref.length - 1) : (_i = 0, []), name = _ref[_i++]; 655 | orig = obj; 656 | for (index = 0, _len = path.length; index < _len; index++) { 657 | part = path[index]; 658 | obj = obj && obj[part]; 659 | } 660 | if (obj != null ? obj[name] : void 0) { 661 | delete obj[name]; 662 | } 663 | return orig; 664 | } else { 665 | for (_j = 0, _len2 = path.length; _j < _len2; _j++) { 666 | part = path[_j]; 667 | obj = obj && obj[part]; 668 | } 669 | return obj; 670 | } 671 | } else if (path === void 0) { 672 | return obj; 673 | } else { 674 | if (remove) { 675 | delete obj[path]; 676 | return obj; 677 | } else { 678 | return obj[path]; 679 | } 680 | } 681 | } 682 | }); 683 | _.mixin({ 684 | parseDate: function(value) { 685 | var date, parts; 686 | date = new Date(value); 687 | if (_.isDate(date)) { 688 | return date; 689 | } 690 | parts = String(value).match(/(\d+)/g); 691 | return new Date(parts[0], (parts[1] || 1) - 1, parts[2] || 1); 692 | }, 693 | isDate: function(obj) { 694 | return !!((obj != null ? obj.getTimezoneOffset : void 0) && obj.setUTCFullYear && !_.isNaN(obj.getTime())); 695 | } 696 | }); 697 | 'use strict'; 698 | /* 699 | * 700 | * Copyright(c) 2011 Vladimir Dronnikov 701 | * MIT Licensed 702 | * 703 | */ 704 | /* 705 | Rewrite of kriszyp's RQL https://github.com/kriszyp/rql 706 | Relies on documentcloud/underscore to normalize JS 707 | */ 708 | operatorMap = { 709 | '=': 'eq', 710 | '==': 'eq', 711 | '>': 'gt', 712 | '>=': 'ge', 713 | '<': 'lt', 714 | '<=': 'le', 715 | '!=': 'ne' 716 | }; 717 | Query = (function() { 718 | function Query(query, parameters) { 719 | var k, leftoverCharacters, removeParentProperty, term, topTerm, v; 720 | if (query == null) { 721 | query = ''; 722 | } 723 | term = this; 724 | term.name = 'and'; 725 | term.args = []; 726 | topTerm = term; 727 | if (_.isObject(query)) { 728 | if (_.isArray(query)) { 729 | topTerm["in"]('id', query); 730 | return; 731 | } else if (query instanceof Query) { 732 | query = query.toString(); 733 | } else { 734 | for (k in query) { 735 | if (!__hasProp.call(query, k)) continue; 736 | v = query[k]; 737 | term = new Query(); 738 | topTerm.args.push(term); 739 | term.name = 'eq'; 740 | term.args = [k, v]; 741 | } 742 | return; 743 | } 744 | } else { 745 | if (typeof query !== 'string') { 746 | throw new URIError('Illegal query'); 747 | } 748 | } 749 | if (query.charAt(0) === '?') { 750 | query = query.substring(1); 751 | } 752 | if (query.indexOf('/') >= 0) { 753 | query = query.replace(/[\+\*\$\-:\w%\._]*\/[\+\*\$\-:\w%\._\/]*/g, function(slashed) { 754 | return '(' + slashed.replace(/\//g, ',') + ')'; 755 | }); 756 | } 757 | query = query.replace(/(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)/g, function(t, property, operator, value) { 758 | if (operator.length < 3) { 759 | if (!(operator in operatorMap)) { 760 | throw new URIError('Illegal operator ' + operator); 761 | } 762 | operator = operatorMap[operator]; 763 | } else { 764 | operator = operator.substring(1, operator.length - 1); 765 | } 766 | return operator + '(' + property + ',' + value + ')'; 767 | }); 768 | if (query.charAt(0) === '?') { 769 | query = query.substring(1); 770 | } 771 | leftoverCharacters = query.replace(/(\))|([&\|,])?([\+\*\$\-:\w%\._]*)(\(?)/g, function(t, closedParen, delim, propertyOrValue, openParen) { 772 | var isArray, newTerm, op; 773 | if (delim) { 774 | if (delim === '&') { 775 | op = 'and'; 776 | } else if (delim === '|') { 777 | op = 'or'; 778 | } 779 | if (op) { 780 | if (!term.name) { 781 | term.name = op; 782 | } else if (term.name !== op) { 783 | throw new Error('Cannot mix conjunctions within a group, use parenthesis around each set of same conjuctions (& and |)'); 784 | } 785 | } 786 | } 787 | if (openParen) { 788 | newTerm = new Query(); 789 | newTerm.name = propertyOrValue; 790 | newTerm.parent = term; 791 | term.args.push(newTerm); 792 | term = newTerm; 793 | } else if (closedParen) { 794 | isArray = !term.name; 795 | term = term.parent; 796 | if (!term) { 797 | throw new URIError('Closing parenthesis without an opening parenthesis'); 798 | } 799 | if (isArray) { 800 | term.args.push(term.args.pop().args); 801 | } 802 | } else if (delim === ',') { 803 | if (term.args.length === 0) { 804 | term.args.push(''); 805 | } 806 | term.args.push(stringToValue(propertyOrValue, parameters)); 807 | } else if (propertyOrValue) { 808 | term.args.push(stringToValue(propertyOrValue, parameters)); 809 | } 810 | return ''; 811 | }); 812 | if (term.parent) { 813 | throw new URIError('Opening parenthesis without a closing parenthesis'); 814 | } 815 | if (leftoverCharacters) { 816 | throw new URIError('Illegal character in query string encountered ' + leftoverCharacters); 817 | } 818 | removeParentProperty = function(obj) { 819 | if (obj != null ? obj.args : void 0) { 820 | delete obj.parent; 821 | _.each(obj.args, removeParentProperty); 822 | } 823 | return obj; 824 | }; 825 | removeParentProperty(topTerm); 826 | } 827 | Query.prototype.toString = function() { 828 | if (this.name === 'and') { 829 | return _.map(this.args, queryToString).join('&'); 830 | } else { 831 | return queryToString(this); 832 | } 833 | }; 834 | Query.prototype.where = function(query) { 835 | this.args = this.args.concat(new Query(query).args); 836 | return this; 837 | }; 838 | Query.prototype.toSQL = function(options) { 839 | if (options == null) { 840 | options = {}; 841 | } 842 | throw Error('Not implemented'); 843 | }; 844 | Query.prototype.toMongo = function(options) { 845 | var result, search, walk; 846 | if (options == null) { 847 | options = {}; 848 | } 849 | walk = function(name, terms) { 850 | var search; 851 | search = {}; 852 | _.each(terms || [], function(term) { 853 | var args, func, key, limit, nested, pm, regex, x, y, _ref; 854 | if (term == null) { 855 | term = {}; 856 | } 857 | func = term.name; 858 | args = term.args; 859 | if (!(func && args)) { 860 | return; 861 | } 862 | if (_.isString((_ref = args[0]) != null ? _ref.name : void 0) && _.isArray(args[0].args)) { 863 | if (_.include(valid_operators, func)) { 864 | nested = walk(func, args); 865 | search['$' + func] = nested; 866 | } 867 | } else { 868 | if (func === 'sort' || func === 'select' || func === 'values') { 869 | if (func === 'values') { 870 | func = 'select'; 871 | options.values = true; 872 | } 873 | pm = plusMinus[func]; 874 | options[func] = {}; 875 | args = _.map(args, function(x) { 876 | if (x === 'id' || x === '+id') { 877 | return '_id'; 878 | } else { 879 | return x; 880 | } 881 | }); 882 | args = _.map(args, function(x) { 883 | if (x === '-id') { 884 | return '-_id'; 885 | } else { 886 | return x; 887 | } 888 | }); 889 | _.each(args, function(x, index) { 890 | var a; 891 | if (_.isArray(x)) { 892 | x = x.join('.'); 893 | } 894 | a = /([-+]*)(.+)/.exec(x); 895 | return options[func][a[2]] = pm[(a[1].charAt(0) === '-') * 1] * (index + 1); 896 | }); 897 | return; 898 | } else if (func === 'limit') { 899 | limit = args; 900 | options.skip = +limit[1] || 0; 901 | options.limit = +limit[0] || Infinity; 902 | options.needCount = true; 903 | return; 904 | } 905 | if (func === 'le') { 906 | func = 'lte'; 907 | } else if (func === 'ge') { 908 | func = 'gte'; 909 | } 910 | key = args[0]; 911 | args = args.slice(1); 912 | if (_.isArray(key)) { 913 | key = key.join('.'); 914 | } 915 | if (String(key).charAt(0) === '$') { 916 | return; 917 | } 918 | if (key === 'id') { 919 | key = '_id'; 920 | } 921 | if (_.include(requires_array, func)) { 922 | args = args[0]; 923 | } else if (func === 'match') { 924 | func = 'eq'; 925 | regex = new RegExp; 926 | regex.compile.apply(regex, args); 927 | args = regex; 928 | } else { 929 | args = args.length === 1 ? args[0] : args.join(); 930 | } 931 | if (func === 'ne' && _.isRegExp(args)) { 932 | func = 'not'; 933 | } 934 | if (_.include(valid_funcs, func)) { 935 | func = '$' + func; 936 | } else { 937 | return; 938 | } 939 | if (Query.convertId && key === '_id') { 940 | if (_.isArray(args)) { 941 | args = args.map(function(x) { 942 | return Query.convertId(x); 943 | }); 944 | } else { 945 | args = Query.convertId(args); 946 | } 947 | } 948 | if (name === 'or') { 949 | if (!_.isArray(search)) { 950 | search = []; 951 | } 952 | x = {}; 953 | if (func === '$eq') { 954 | x[key] = args; 955 | } else { 956 | y = {}; 957 | y[func] = args; 958 | x[key] = y; 959 | } 960 | search.push(x); 961 | } else { 962 | if (search[key] === void 0) { 963 | search[key] = {}; 964 | } 965 | if (_.isObject(search[key]) && !_.isArray(search[key])) { 966 | search[key][func] = args; 967 | } 968 | if (func === '$eq') { 969 | search[key] = args; 970 | } 971 | } 972 | } 973 | }); 974 | return search; 975 | }; 976 | search = walk(this.name, this.args); 977 | if (options.select) { 978 | options.fields = options.select; 979 | delete options.select; 980 | } 981 | result = { 982 | meta: options, 983 | search: search 984 | }; 985 | if (this.error) { 986 | result.error = this.error; 987 | } 988 | return result; 989 | }; 990 | return Query; 991 | })(); 992 | stringToValue = function(string, parameters) { 993 | var converter, param_index, parts; 994 | converter = converters["default"]; 995 | if (string.charAt(0) === '$') { 996 | param_index = parseInt(string.substring(1), 10) - 1; 997 | if (param_index >= 0 && parameters) { 998 | return parameters[param_index]; 999 | } else { 1000 | return; 1001 | } 1002 | } 1003 | if (string.indexOf(':') >= 0) { 1004 | parts = string.split(':', 2); 1005 | converter = converters[parts[0]]; 1006 | if (!converter) { 1007 | throw new URIError('Unknown converter ' + parts[0]); 1008 | } 1009 | string = parts[1]; 1010 | } 1011 | return converter(string); 1012 | }; 1013 | queryToString = function(part) { 1014 | var mapped; 1015 | if (_.isArray(part)) { 1016 | mapped = _.map(part, function(arg) { 1017 | return queryToString(arg); 1018 | }); 1019 | return '(' + mapped.join(',') + ')'; 1020 | } else if (part && part.name && part.args) { 1021 | mapped = _.map(part.args, function(arg) { 1022 | return queryToString(arg); 1023 | }); 1024 | return part.name + '(' + mapped.join(',') + ')'; 1025 | } else { 1026 | return encodeValue(part); 1027 | } 1028 | }; 1029 | encodeString = function(s) { 1030 | if (_.isString(s)) { 1031 | s = encodeURIComponent(s); 1032 | if (s.match(/[\(\)]/)) { 1033 | s = s.replace('(', '%28').replace(')', '%29'); 1034 | } 1035 | } 1036 | return s; 1037 | }; 1038 | encodeValue = function(val) { 1039 | var encoded, i, type; 1040 | if (val === null) { 1041 | return 'null'; 1042 | } else if (typeof val === 'undefined') { 1043 | return val; 1044 | } 1045 | if (val !== converters["default"]('' + (val.toISOString && val.toISOString() || val.toString()))) { 1046 | if (_.isRegExp(val)) { 1047 | val = val.toString(); 1048 | i = val.lastIndexOf('/'); 1049 | type = val.substring(i).indexOf('i') >= 0 ? 're' : 'RE'; 1050 | val = encodeString(val.substring(1, i)); 1051 | encoded = true; 1052 | } else if (_.isDate(val)) { 1053 | type = 'epoch'; 1054 | val = val.getTime(); 1055 | encoded = true; 1056 | } else if (_.isString(type)) { 1057 | type = 'string'; 1058 | val = encodeString(val); 1059 | encoded = true; 1060 | } else { 1061 | type = typeof val; 1062 | } 1063 | val = [type, val].join(':'); 1064 | } 1065 | if (!encoded && _.isString(val)) { 1066 | val = encodeString(val); 1067 | } 1068 | return val; 1069 | }; 1070 | autoConverted = { 1071 | 'true': true, 1072 | 'false': false, 1073 | 'null': null, 1074 | 'undefined': void 0, 1075 | 'Infinity': Infinity, 1076 | '-Infinity': -Infinity 1077 | }; 1078 | converters = { 1079 | auto: function(string) { 1080 | var number; 1081 | if (string in autoConverted) { 1082 | return autoConverted[string]; 1083 | } 1084 | number = +string; 1085 | if (_.isNaN(number) || number.toString() !== string) { 1086 | string = decodeURIComponent(string); 1087 | return string; 1088 | } 1089 | return number; 1090 | }, 1091 | number: function(x) { 1092 | var number; 1093 | number = +x; 1094 | if (_.isNaN(number)) { 1095 | throw new URIError('Invalid number ' + x); 1096 | } 1097 | return number; 1098 | }, 1099 | epoch: function(x) { 1100 | var date; 1101 | date = new Date(+x); 1102 | if (!_.isDate(date)) { 1103 | throw new URIError('Invalid date ' + x); 1104 | } 1105 | return date; 1106 | }, 1107 | isodate: function(x) { 1108 | var date; 1109 | date = '0000'.substr(0, 4 - x.length) + x; 1110 | date += '0000-01-01T00:00:00Z'.substring(date.length); 1111 | return converters.date(date); 1112 | }, 1113 | date: function(x) { 1114 | var date, isoDate; 1115 | isoDate = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(x); 1116 | if (isoDate) { 1117 | date = new Date(Date.UTC(+isoDate[1], +isoDate[2] - 1, +isoDate[3], +isoDate[4], +isoDate[5], +isoDate[6])); 1118 | } else { 1119 | date = _.parseDate(x); 1120 | } 1121 | if (!_.isDate(date)) { 1122 | throw new URIError('Invalid date ' + x); 1123 | } 1124 | return date; 1125 | }, 1126 | boolean: function(x) { 1127 | if (x === 'false') { 1128 | return false; 1129 | } else { 1130 | return !!x; 1131 | } 1132 | }, 1133 | string: function(string) { 1134 | return decodeURIComponent(string); 1135 | }, 1136 | re: function(x) { 1137 | return new RegExp(decodeURIComponent(x), 'i'); 1138 | }, 1139 | RE: function(x) { 1140 | return new RegExp(decodeURIComponent(x)); 1141 | }, 1142 | glob: function(x) { 1143 | var s; 1144 | s = decodeURIComponent(x).replace(/([\\|\||\(|\)|\[|\{|\^|\$|\*|\+|\?|\.|\<|\>])/g, function(x) { 1145 | return '\\' + x; 1146 | }); 1147 | s = s.replace(/\\\*/g, '.*').replace(/\\\?/g, '.?'); 1148 | s = s.substring(0, 2) !== '.*' ? '^' + s : s.substring(2); 1149 | s = s.substring(s.length - 2) !== '.*' ? s + '$' : s.substring(0, s.length - 2); 1150 | return new RegExp(s, 'i'); 1151 | } 1152 | }; 1153 | converters["default"] = converters.auto; 1154 | _.each(['eq', 'ne', 'le', 'ge', 'lt', 'gt', 'between', 'in', 'nin', 'contains', 'ncontains', 'or', 'and'], function(op) { 1155 | return Query.prototype[op] = function() { 1156 | var args; 1157 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 1158 | this.args.push({ 1159 | name: op, 1160 | args: args 1161 | }); 1162 | return this; 1163 | }; 1164 | }); 1165 | parse = function(query, parameters) { 1166 | var q; 1167 | try { 1168 | q = new Query(query, parameters); 1169 | } catch (x) { 1170 | q = new Query; 1171 | q.error = x.message; 1172 | } 1173 | return q; 1174 | }; 1175 | valid_funcs = ['eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'not', 'mod', 'all', 'size', 'exists', 'type', 'elemMatch']; 1176 | requires_array = ['in', 'nin', 'all', 'mod']; 1177 | valid_operators = ['or', 'and', 'not']; 1178 | plusMinus = { 1179 | sort: [1, -1], 1180 | select: [1, 0] 1181 | }; 1182 | jsOperatorMap = { 1183 | 'eq': '===', 1184 | 'ne': '!==', 1185 | 'le': '<=', 1186 | 'ge': '>=', 1187 | 'lt': '<', 1188 | 'gt': '>' 1189 | }; 1190 | operators = { 1191 | and: function() { 1192 | var cond, conditions, obj, _i, _len; 1193 | obj = arguments[0], conditions = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 1194 | for (_i = 0, _len = conditions.length; _i < _len; _i++) { 1195 | cond = conditions[_i]; 1196 | if (_.isFunction(cond)) { 1197 | obj = cond(obj); 1198 | } 1199 | } 1200 | return obj; 1201 | }, 1202 | or: function() { 1203 | var cond, conditions, list, obj, _i, _len; 1204 | obj = arguments[0], conditions = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 1205 | list = []; 1206 | for (_i = 0, _len = conditions.length; _i < _len; _i++) { 1207 | cond = conditions[_i]; 1208 | if (_.isFunction(cond)) { 1209 | list = list.concat(cond(obj)); 1210 | } 1211 | } 1212 | return _.uniq(list); 1213 | }, 1214 | limit: function(list, limit, start) { 1215 | if (start == null) { 1216 | start = 0; 1217 | } 1218 | return list.slice(start, start + limit); 1219 | }, 1220 | slice: function(list, start, end) { 1221 | if (start == null) { 1222 | start = 0; 1223 | } 1224 | if (end == null) { 1225 | end = Infinity; 1226 | } 1227 | return list.slice(start, end); 1228 | }, 1229 | pick: function() { 1230 | var exclude, include, list, props; 1231 | list = arguments[0], props = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 1232 | include = []; 1233 | exclude = []; 1234 | _.each(props, function(x, index) { 1235 | var a, leading; 1236 | leading = _.isArray(x) ? x[0] : x; 1237 | a = /([-+]*)(.+)/.exec(leading); 1238 | if (_.isArray(x)) { 1239 | x[0] = a[2]; 1240 | } else { 1241 | x = a[2]; 1242 | } 1243 | if (a[1].charAt(0) === '-') { 1244 | return exclude.push(x); 1245 | } else { 1246 | return include.push(x); 1247 | } 1248 | }); 1249 | return _.map(list, function(item) { 1250 | var i, n, s, selected, t, value, x, _i, _j, _k, _len, _len2, _len3, _ref; 1251 | if (_.isEmpty(include)) { 1252 | selected = _.clone(item); 1253 | } else { 1254 | selected = {}; 1255 | for (_i = 0, _len = include.length; _i < _len; _i++) { 1256 | x = include[_i]; 1257 | value = _.drill(item, x); 1258 | if (value === void 0) { 1259 | continue; 1260 | } 1261 | if (_.isArray(x)) { 1262 | t = s = selected; 1263 | n = x.slice(-1); 1264 | for (_j = 0, _len2 = x.length; _j < _len2; _j++) { 1265 | i = x[_j]; 1266 | if ((_ref = t[i]) != null) { 1267 | _ref; 1268 | } else { 1269 | t[i] = {}; 1270 | }; 1271 | s = t; 1272 | t = t[i]; 1273 | } 1274 | s[n] = value; 1275 | } else { 1276 | selected[x] = value; 1277 | } 1278 | } 1279 | } 1280 | for (_k = 0, _len3 = exclude.length; _k < _len3; _k++) { 1281 | x = exclude[_k]; 1282 | _.drill(selected, x, true); 1283 | } 1284 | return selected; 1285 | }); 1286 | }, 1287 | values: function() { 1288 | return _.map(operators.pick.apply(this, arguments), _.values); 1289 | }, 1290 | sort: function() { 1291 | var list, order, props; 1292 | list = arguments[0], props = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 1293 | order = []; 1294 | _.each(props, function(x, index) { 1295 | var a, leading; 1296 | leading = _.isArray(x) ? x[0] : x; 1297 | a = /([-+]*)(.+)/.exec(leading); 1298 | if (_.isArray(x)) { 1299 | x[0] = a[2]; 1300 | } else { 1301 | x = a[2]; 1302 | } 1303 | if (a[1].charAt(0) === '-') { 1304 | return order.push({ 1305 | attr: x, 1306 | order: -1 1307 | }); 1308 | } else { 1309 | return order.push({ 1310 | attr: x, 1311 | order: 1 1312 | }); 1313 | } 1314 | }); 1315 | return list.sort(function(a, b) { 1316 | var prop, va, vb, _i, _len; 1317 | for (_i = 0, _len = order.length; _i < _len; _i++) { 1318 | prop = order[_i]; 1319 | va = _.drill(a, prop.attr); 1320 | vb = _.drill(b, prop.attr); 1321 | if (va > vb) { 1322 | return prop.order; 1323 | } else { 1324 | if (va !== vb) { 1325 | return -prop.order; 1326 | } 1327 | } 1328 | } 1329 | return 0; 1330 | }); 1331 | }, 1332 | match: function(list, prop, regex) { 1333 | if (!_.isRegExp(regex)) { 1334 | regex = new RegExp(regex, 'i'); 1335 | } 1336 | return _.select(list, function(x) { 1337 | return regex.test(_.drill(x, prop)); 1338 | }); 1339 | }, 1340 | nmatch: function(list, prop, regex) { 1341 | if (!_.isRegExp(regex)) { 1342 | regex = new RegExp(regex, 'i'); 1343 | } 1344 | return _.select(list, function(x) { 1345 | return !regex.test(_.drill(x, prop)); 1346 | }); 1347 | }, 1348 | "in": function(list, prop, values) { 1349 | values = _.ensureArray(values); 1350 | return _.select(list, function(x) { 1351 | return _.include(values, _.drill(x, prop)); 1352 | }); 1353 | }, 1354 | nin: function(list, prop, values) { 1355 | values = _.ensureArray(values); 1356 | return _.select(list, function(x) { 1357 | return !_.include(values, _.drill(x, prop)); 1358 | }); 1359 | }, 1360 | contains: function(list, prop, value) { 1361 | return _.select(list, function(x) { 1362 | return _.include(_.drill(x, prop), value); 1363 | }); 1364 | }, 1365 | ncontains: function(list, prop, value) { 1366 | return _.select(list, function(x) { 1367 | return !_.include(_.drill(x, prop), value); 1368 | }); 1369 | }, 1370 | between: function(list, prop, minInclusive, maxExclusive) { 1371 | return _.select(list, function(x) { 1372 | var _ref; 1373 | return (minInclusive <= (_ref = _.drill(x, prop)) && _ref < maxExclusive); 1374 | }); 1375 | }, 1376 | nbetween: function(list, prop, minInclusive, maxExclusive) { 1377 | return _.select(list, function(x) { 1378 | var _ref; 1379 | return !((minInclusive <= (_ref = _.drill(x, prop)) && _ref < maxExclusive)); 1380 | }); 1381 | } 1382 | }; 1383 | operators.select = operators.pick; 1384 | operators.out = operators.nin; 1385 | operators.excludes = operators.ncontains; 1386 | operators.distinct = _.uniq; 1387 | stringify = function(str) { 1388 | return '"' + String(str).replace(/"/g, '\\"') + '"'; 1389 | }; 1390 | query = function(list, query, options) { 1391 | var expr, queryToJS; 1392 | if (options == null) { 1393 | options = {}; 1394 | } 1395 | query = parse(query, options.parameters); 1396 | if (query.error) { 1397 | return []; 1398 | } 1399 | queryToJS = function(value) { 1400 | var condition, escaped, item, p, path, prm, testValue, _i, _len; 1401 | if (_.isObject(value) && !_.isRegExp(value)) { 1402 | if (_.isArray(value)) { 1403 | return '[' + _.map(value, queryToJS) + ']'; 1404 | } else { 1405 | if (value.name in jsOperatorMap) { 1406 | path = value.args[0]; 1407 | prm = value.args[1]; 1408 | item = 'item'; 1409 | if (prm === void 0) { 1410 | prm = path; 1411 | } else if (_.isArray(path)) { 1412 | escaped = []; 1413 | for (_i = 0, _len = path.length; _i < _len; _i++) { 1414 | p = path[_i]; 1415 | escaped.push(stringify(p)); 1416 | item += '&&item[' + escaped.join('][') + ']'; 1417 | } 1418 | } else { 1419 | item += '&&item[' + stringify(path) + ']'; 1420 | } 1421 | testValue = queryToJS(prm); 1422 | if (_.isRegExp(testValue)) { 1423 | condition = testValue + (".test(" + item + ")"); 1424 | if (value.name !== 'eq') { 1425 | condition = "!(" + condition + ")"; 1426 | } 1427 | } else { 1428 | condition = item + jsOperatorMap[value.name] + testValue; 1429 | } 1430 | return "function(list){return _.select(list,function(item){return " + condition + ";});}"; 1431 | } else if (value.name in operators) { 1432 | return ("function(list){return operators['" + value.name + "'](") + ['list'].concat(_.map(value.args, queryToJS)).join(',') + ');}'; 1433 | } else { 1434 | return "function(list){return _.select(list,function(item){return false;});}"; 1435 | } 1436 | } 1437 | } else { 1438 | if (_.isString(value)) { 1439 | return stringify(value); 1440 | } else { 1441 | return value; 1442 | } 1443 | } 1444 | }; 1445 | expr = queryToJS(query).slice(15, -1); 1446 | if (list) { 1447 | return (new Function('list, operators', expr))(list, operators); 1448 | } else { 1449 | return expr; 1450 | } 1451 | }; 1452 | _.mixin({ 1453 | rql: parse, 1454 | query: query 1455 | }); 1456 | 'use strict'; 1457 | /* 1458 | 1459 | JSONSchema Validator - Validates JavaScript objects using JSON Schemas 1460 | (http://www.json.com/json-schema-proposal/) 1461 | 1462 | Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com) 1463 | Copyright (c) 2011 Vladimir Dronnikov dronnikov@gmail.com 1464 | 1465 | Licensed under the MIT (MIT-LICENSE.txt) license 1466 | 1467 | */ 1468 | /* 1469 | * 1470 | * Copyright(c) 2011 Vladimir Dronnikov 1471 | * MIT Licensed 1472 | * 1473 | */ 1474 | /* 1475 | Rewrite of kriszyp's json-schema validator https://github.com/kriszyp/json-schema 1476 | Relies on documentcloud/underscore to normalize JS 1477 | */ 1478 | coerce = function(value, type) { 1479 | var date; 1480 | if (type === 'string') { 1481 | value = value != null ? String(value) : ''; 1482 | } else if (type === 'number' || type === 'integer') { 1483 | if (!_.isNaN(value)) { 1484 | value = Number(value); 1485 | if (type === 'integer') { 1486 | value = Math.floor(value); 1487 | } 1488 | } 1489 | } else if (type === 'boolean') { 1490 | value = value === 'false' ? false : !!value; 1491 | } else if (type === 'null') { 1492 | value = null; 1493 | } else if (type === 'object') { 1494 | if (typeof JSON !== "undefined" && JSON !== null ? JSON.parse : void 0) { 1495 | try { 1496 | value = JSON.parse(value); 1497 | } catch (err) { 1498 | 1499 | } 1500 | } 1501 | } else if (type === 'array') { 1502 | value = _.ensureArray(value); 1503 | } else if (type === 'date') { 1504 | date = _.parseDate(value); 1505 | if (_.isDate(date)) { 1506 | value = date; 1507 | } 1508 | } 1509 | return value; 1510 | }; 1511 | validate = function(instance, schema, options, callback) { 1512 | var async, asyncs, checkObj, checkProp, errors, i, len, self, _changing, _fn, _len; 1513 | if (options == null) { 1514 | options = {}; 1515 | } 1516 | self = this; 1517 | _changing = options.changing; 1518 | asyncs = []; 1519 | errors = []; 1520 | checkProp = function(value, schema, path, i) { 1521 | var addError, checkType, enumeration, itemsIsArray, propDef, v, _len; 1522 | if (path) { 1523 | if (_.isNumber(i)) { 1524 | path += '[' + i + ']'; 1525 | } else if (i === void 0) { 1526 | path += ''; 1527 | } else { 1528 | path += '.' + i; 1529 | } 1530 | } else { 1531 | path += i; 1532 | } 1533 | addError = function(message) { 1534 | return errors.push({ 1535 | property: path, 1536 | message: message 1537 | }); 1538 | }; 1539 | if ((typeof schema !== 'object' || _.isArray(schema)) && (path || typeof schema !== 'function') && !(schema != null ? schema.type : void 0)) { 1540 | if (_.isFunction(schema)) { 1541 | if (!(value instanceof schema)) { 1542 | addError('type'); 1543 | } 1544 | } else if (schema) { 1545 | addError('invalid'); 1546 | } 1547 | return null; 1548 | } 1549 | if (_changing && schema.readonly) { 1550 | addError('readonly'); 1551 | } 1552 | if (schema["extends"]) { 1553 | checkProp(value, schema["extends"], path, i); 1554 | } 1555 | checkType = function(type, value) { 1556 | var priorErrors, t, theseErrors, unionErrors, _i, _len; 1557 | if (type) { 1558 | if (typeof type === 'string' && type !== 'any' && (type == 'null' ? value !== null : typeof value !== type) && 1559 | !(type === 'array' && _.isArray(value)) && 1560 | !(type === 'date' && _.isDate(value)) && 1561 | !(type === 'integer' && value%1===0)) { 1562 | return [ 1563 | { 1564 | property: path, 1565 | message: 'type' 1566 | } 1567 | ]; 1568 | } 1569 | if (_.isArray(type)) { 1570 | unionErrors = []; 1571 | for (_i = 0, _len = type.length; _i < _len; _i++) { 1572 | t = type[_i]; 1573 | unionErrors = checkType(t, value); 1574 | if (!unionErrors.length) { 1575 | break; 1576 | } 1577 | } 1578 | if (unionErrors.length) { 1579 | return unionErrors; 1580 | } 1581 | } else if (typeof type === 'object') { 1582 | priorErrors = errors; 1583 | errors = []; 1584 | checkProp(value, type, path); 1585 | theseErrors = errors; 1586 | errors = priorErrors; 1587 | return theseErrors; 1588 | } 1589 | } 1590 | return []; 1591 | }; 1592 | if (value === void 0) { 1593 | if ((!schema.optional || typeof schema.optional === 'object' && !schema.optional[options.flavor]) && !schema.get && !(schema["default"] != null)) { 1594 | addError('required'); 1595 | } 1596 | } else { 1597 | errors = errors.concat(checkType(schema.type, value)); 1598 | if (schema.disallow && !checkType(schema.disallow, value).length) { 1599 | addError('disallowed'); 1600 | } 1601 | if (value !== null) { 1602 | if (_.isArray(value)) { 1603 | if (schema.items) { 1604 | itemsIsArray = _.isArray(schema.items); 1605 | propDef = schema.items; 1606 | for (i = 0, _len = value.length; i < _len; i++) { 1607 | v = value[i]; 1608 | if (itemsIsArray) { 1609 | propDef = schema.items[i]; 1610 | } 1611 | if (options.coerce && propDef.type) { 1612 | value[i] = coerce(v, propDef.type); 1613 | } 1614 | errors.concat(checkProp(v, propDef, path, i)); 1615 | } 1616 | } 1617 | if (schema.minItems && value.length < schema.minItems) { 1618 | addError('minItems'); 1619 | } 1620 | if (schema.maxItems && value.length > schema.maxItems) { 1621 | addError('maxItems'); 1622 | } 1623 | } else if (schema.properties || schema.additionalProperties) { 1624 | errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties)); 1625 | } 1626 | if (_.isString(value)) { 1627 | if (schema.pattern && !value.match(schema.pattern)) { 1628 | addError('pattern'); 1629 | } 1630 | if (schema.maxLength && value.length > schema.maxLength) { 1631 | addError('maxLength'); 1632 | } 1633 | if (schema.minLength && value.length < schema.minLength) { 1634 | addError('minLength'); 1635 | } 1636 | } 1637 | if (schema.minimum !== void 0 && typeof value === typeof schema.minimum && schema.minimum > value) { 1638 | addError('minimum'); 1639 | } 1640 | if (schema.maximum !== void 0 && typeof value === typeof schema.maximum && schema.maximum < value) { 1641 | addError('maximum'); 1642 | } 1643 | if (schema["enum"]) { 1644 | enumeration = schema["enum"]; 1645 | if (_.isFunction(enumeration)) { 1646 | if (enumeration.length === 2) { 1647 | asyncs.push({ 1648 | value: value, 1649 | path: path, 1650 | fetch: enumeration 1651 | }); 1652 | } else if (enumeration.length === 1) { 1653 | if (!enumeration.call(self, value)) { 1654 | addError('enum'); 1655 | } 1656 | } else { 1657 | enumeration = enumeration.call(self); 1658 | if (!_.include(enumeration, value)) { 1659 | addError('enum'); 1660 | } 1661 | } 1662 | } else { 1663 | if (!_.include(enumeration, value)) { 1664 | addError('enum'); 1665 | } 1666 | } 1667 | } 1668 | if (_.isNumber(schema.maxDecimal) && (new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")).test(value)) { 1669 | addError('digits'); 1670 | } 1671 | } 1672 | } 1673 | return null; 1674 | }; 1675 | checkObj = function(instance, objTypeDef, path, additionalProp) { 1676 | var i, propDef, requires, value, _ref, _ref2, _ref3; 1677 | if (objTypeDef == null) { 1678 | objTypeDef = {}; 1679 | } 1680 | if (_.isObject(objTypeDef)) { 1681 | if (typeof instance !== 'object' || _.isArray(instance)) { 1682 | errors.push({ 1683 | property: path, 1684 | message: 'type' 1685 | }); 1686 | } 1687 | for (i in objTypeDef) { 1688 | if (!__hasProp.call(objTypeDef, i)) continue; 1689 | propDef = objTypeDef[i]; 1690 | value = instance[i]; 1691 | if ('value' in propDef && ((_ref = options.flavor) === 'add' || _ref === 'update')) { 1692 | value = instance[i] = propDef.value; 1693 | } 1694 | if (value === void 0 && options.existingOnly) { 1695 | continue; 1696 | } 1697 | if (options.veto && (propDef.veto === true || typeof propDef.veto === 'object' && propDef.veto[options.flavor])) { 1698 | delete instance[i]; 1699 | continue; 1700 | } 1701 | if (((_ref2 = options.flavor) === 'query' || _ref2 === 'get') && !options.coerce) { 1702 | continue; 1703 | } 1704 | if (value === void 0 && (propDef["default"] != null) && options.flavor === 'add') { 1705 | value = instance[i] = propDef["default"]; 1706 | } 1707 | if (value === void 0 && options.flavor !== 'add') { 1708 | delete instance[i]; 1709 | continue; 1710 | } 1711 | if (options.coerce && propDef.type && i in instance && value !== void 0) { 1712 | value = coerce(value, propDef.type); 1713 | instance[i] = value; 1714 | } 1715 | if (value === void 0 && propDef.optional) { 1716 | delete instance[i]; 1717 | continue; 1718 | } 1719 | checkProp(value, propDef, path, i); 1720 | } 1721 | } 1722 | for (i in instance) { 1723 | value = instance[i]; 1724 | if (i in instance && !objTypeDef[i] && (additionalProp === false || options.removeAdditionalProps)) { 1725 | if (options.removeAdditionalProps) { 1726 | delete instance[i]; 1727 | continue; 1728 | } else { 1729 | errors.push({ 1730 | property: path, 1731 | message: 'unspecifed' 1732 | }); 1733 | } 1734 | } 1735 | requires = (_ref3 = objTypeDef[i]) != null ? _ref3.requires : void 0; 1736 | if (requires && !requires in instance) { 1737 | errors.push({ 1738 | property: path, 1739 | message: 'requires' 1740 | }); 1741 | } 1742 | if ((additionalProp != null ? additionalProp.type : void 0) && !objTypeDef[i]) { 1743 | if (options.coerce && additionalProp.type) { 1744 | value = coerce(value, additionalProp.type); 1745 | instance[i] = value; 1746 | checkProp(value, additionalProp, path, i); 1747 | } 1748 | } 1749 | if (!_changing && (value != null ? value.$schema : void 0)) { 1750 | errors = errors.concat(checkProp(value, value.$schema, path, i)); 1751 | } 1752 | } 1753 | return errors; 1754 | }; 1755 | if (schema) { 1756 | checkProp(instance, schema, '', _changing || ''); 1757 | } 1758 | if (!_changing && (instance != null ? instance.$schema : void 0)) { 1759 | checkProp(instance, instance.$schema, '', ''); 1760 | } 1761 | len = asyncs.length; 1762 | if (callback && len) { 1763 | _fn = function(async) { 1764 | return async.fetch.call(self, async.value, function(err) { 1765 | if (err) { 1766 | errors.push({ 1767 | property: async.path, 1768 | message: 'enum' 1769 | }); 1770 | } 1771 | len -= 1; 1772 | if (!len) { 1773 | return callback(errors.length && errors || null, instance); 1774 | } 1775 | }); 1776 | }; 1777 | for (i = 0, _len = asyncs.length; i < _len; i++) { 1778 | async = asyncs[i]; 1779 | _fn(async); 1780 | } 1781 | } else if (callback) { 1782 | callback(errors.length && errors || null, instance); 1783 | } else { 1784 | return errors.length && errors || null; 1785 | } 1786 | }; 1787 | _.mixin({ 1788 | coerce: coerce, 1789 | validate: validate 1790 | }); 1791 | }).call(this); 1792 | --------------------------------------------------------------------------------