|
|
|
@ -9,7 +9,8 @@ var dateToJSON = function () { return { $$date: this.getTime() }; } |
|
|
|
|
, util = require('util') |
|
|
|
|
, _ = require('underscore') |
|
|
|
|
, modifierFunctions = {} |
|
|
|
|
, matcherFunctions = {} |
|
|
|
|
, comparisonFunctions = {} |
|
|
|
|
, logicalOperators = {} |
|
|
|
|
; |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -230,6 +231,24 @@ function modify (obj, updateQuery) { |
|
|
|
|
// Finding documents
|
|
|
|
|
// ==============================================================
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Get a value from object with dot notation |
|
|
|
|
* @param {Object} obj |
|
|
|
|
* @param {String} field |
|
|
|
|
*/ |
|
|
|
|
function getDotValue (obj, field) { |
|
|
|
|
var fieldParts = typeof field === 'string' ? field.split('.') : field; |
|
|
|
|
|
|
|
|
|
if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match
|
|
|
|
|
|
|
|
|
|
if (fieldParts.length === 1) { |
|
|
|
|
return obj[fieldParts[0]]; |
|
|
|
|
} else { |
|
|
|
|
return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Check whether 'things' are equal |
|
|
|
|
* Things are defined as any native types (string, number, boolean, null, date) and objects |
|
|
|
@ -268,28 +287,22 @@ function areThingsEqual (a, b) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Get a value from object with dot notation |
|
|
|
|
* @param {Object} obj |
|
|
|
|
* @param {String} field |
|
|
|
|
*/ |
|
|
|
|
function getDotValue (obj, field) { |
|
|
|
|
var fieldParts = typeof field === 'string' ? field.split('.') : field; |
|
|
|
|
comparisonFunctions.$lt = function (a, b) { |
|
|
|
|
if (typeof a !== 'string' && typeof a !== 'number' && !util.isDate(a) && |
|
|
|
|
typeof b !== 'string' && typeof b !== 'number' && !util.isDate(b)) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!obj) { return undefined; } // field cannot be empty so that means we should return undefined so that nothing can match
|
|
|
|
|
if (typeof a !== typeof b) { return false; } |
|
|
|
|
|
|
|
|
|
if (fieldParts.length === 1) { |
|
|
|
|
return obj[fieldParts[0]]; |
|
|
|
|
} else { |
|
|
|
|
return getDotValue(obj[fieldParts[0]], fieldParts.slice(1)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return a < b; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Match any of the subqueries |
|
|
|
|
*/ |
|
|
|
|
matcherFunctions.$or = function (obj, query) { |
|
|
|
|
logicalOperators.$or = function (obj, query) { |
|
|
|
|
var i; |
|
|
|
|
|
|
|
|
|
if (!util.isArray(query)) { throw "$or operator used without an array"; } |
|
|
|
@ -305,7 +318,7 @@ matcherFunctions.$or = function (obj, query) { |
|
|
|
|
/** |
|
|
|
|
* Match all of the subqueries |
|
|
|
|
*/ |
|
|
|
|
matcherFunctions.$and = function (obj, query) { |
|
|
|
|
logicalOperators.$and = function (obj, query) { |
|
|
|
|
var i; |
|
|
|
|
|
|
|
|
|
if (!util.isArray(query)) { throw "$and operator used without an array"; } |
|
|
|
@ -342,16 +355,18 @@ function match (obj, query) { |
|
|
|
|
function matchQueryPart (obj, queryKey, queryValue) { |
|
|
|
|
var objValue = getDotValue(obj, queryKey) |
|
|
|
|
, i |
|
|
|
|
, keys, firstChars, dollarFirstChars |
|
|
|
|
; |
|
|
|
|
|
|
|
|
|
// Query part begins with a logical operator: apply it
|
|
|
|
|
if (queryKey[0] === '$') { |
|
|
|
|
if (!matcherFunctions[queryKey]) { throw "Unknown query operator " + queryKey; } |
|
|
|
|
if (!logicalOperators[queryKey]) { throw "Unknown logical operator " + queryKey; } |
|
|
|
|
|
|
|
|
|
return matcherFunctions[queryKey](obj, queryValue); |
|
|
|
|
return logicalOperators[queryKey](obj, queryValue); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Check if the object value is an array and we need to rewrite the query
|
|
|
|
|
// Check if the object value is an array treat it as an array of { obj, query }
|
|
|
|
|
// Where there needs to be at least one match
|
|
|
|
|
if (util.isArray(objValue)) { |
|
|
|
|
for (i = 0; i < objValue.length; i += 1) { |
|
|
|
|
if (match({ queryKey: objValue[i] }, { queryKey: queryValue })) { return true; } |
|
|
|
@ -359,7 +374,35 @@ function matchQueryPart (obj, queryKey, queryValue) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Normal field matching
|
|
|
|
|
// queryValue is an actual object. Determine whether it contains comparison operators
|
|
|
|
|
// or only normal fields. Mixed objects are not allowed
|
|
|
|
|
if (queryValue !== null && typeof queryValue === 'object') { |
|
|
|
|
keys = Object.keys(queryValue); |
|
|
|
|
firstChars = _.map(keys, function (item) { return item[0]; }) |
|
|
|
|
dollarFirstChars = _.filter(firstChars, function (c) { return c === '$'; }) |
|
|
|
|
|
|
|
|
|
if (dollarFirstChars.length !== 0 && dollarFirstChars.length !== firstChars.length) { |
|
|
|
|
throw "You cannot mix operators and normal fields"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// queryValue is an object of this form: { $comparisonOperator1: value1, ... }
|
|
|
|
|
if (dollarFirstChars.length > 0) { |
|
|
|
|
for (i = 0; i < keys.length; i += 1) { |
|
|
|
|
if (!comparisonFunctions[keys[i]]) { throw "Unknown comparison function " + keys[i]; } |
|
|
|
|
|
|
|
|
|
if (!comparisonFunctions[keys[i]](objValue, queryValue[keys[i]])) { return false; } |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Query part if of the form { field: $logicaloperator }
|
|
|
|
|
//if (logicalOperators[queryValue]) { throw "Can't use logical operator " + queryValue + " here"; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// queryValue is either a native value or a normal object
|
|
|
|
|
// Simple matching is possible
|
|
|
|
|
if (!areThingsEqual(objValue, queryValue)) { return false; } |
|
|
|
|
|
|
|
|
|
return true; |
|
|
|
|