/ * *
* Handle models ( i . e . docs )
* Serialization / deserialization
* Copying
* Querying , update
* /
var util = require ( 'util' )
, _ = require ( 'underscore' )
, modifierFunctions = { }
, lastStepModifierFunctions = { }
, comparisonFunctions = { }
, logicalOperators = { }
, arrayComparisonFunctions = { }
;
/ * *
* Check a key , throw an error if the key is non valid
* @ param { String } k key
* @ param { Model } v value , needed to treat the Date edge case
* Non - treatable edge cases here : if part of the object if of the form { $$date : number } or { $$deleted : true }
* Its serialized - then - deserialized version it will transformed into a Date object
* But you really need to want it to trigger such behaviour , even when warned not to use '$' at the beginning of the field names ...
* /
function checkKey ( k , v ) {
if ( typeof k === 'number' ) {
k = k . toString ( ) ;
}
if ( k [ 0 ] === '$' && ! ( k === '$$date' && typeof v === 'number' ) && ! ( k === '$$deleted' && v === true ) && ! ( k === '$$indexCreated' ) && ! ( k === '$$indexRemoved' ) ) {
throw new Error ( 'Field names cannot begin with the $ character' ) ;
}
if ( k . indexOf ( '.' ) !== - 1 ) {
throw new Error ( 'Field names cannot contain a .' ) ;
}
}
/ * *
* Check a DB object and throw an error if it ' s not valid
* Works by applying the above checkKey function to all fields recursively
* /
function checkObject ( obj ) {
if ( util . isArray ( obj ) ) {
obj . forEach ( function ( o ) {
checkObject ( o ) ;
} ) ;
}
if ( typeof obj === 'object' && obj !== null ) {
Object . keys ( obj ) . forEach ( function ( k ) {
checkKey ( k , obj [ k ] ) ;
checkObject ( obj [ k ] ) ;
} ) ;
}
}
/ * *
* Serialize an object to be persisted to a one - line string
* For serialization / deserialization , we use the native JSON parser and not eval or Function
* That gives us less freedom but data entered in the database may come from users
* so eval and the like are not safe
* Accepted primitive types : Number , String , Boolean , Date , null
* Accepted secondary types : Objects , Arrays
* /
function serialize ( obj ) {
var res ;
res = JSON . stringify ( obj , function ( k , v ) {
checkKey ( k , v ) ;
if ( v === undefined ) { return undefined ; }
if ( v === null ) { return null ; }
// Hackish way of checking if object is Date (this way it works between execution contexts in node-webkit).
// We can't use value directly because for dates it is already string in this function (date.toJSON was already called), so we use this
if ( typeof this [ k ] . getTime === 'function' ) { return { $$date : this [ k ] . getTime ( ) } ; }
return v ;
} ) ;
return res ;
}
/ * *
* From a one - line representation of an object generate by the serialize function
* Return the object itself
* /
function deserialize ( rawData ) {
return JSON . parse ( rawData , function ( k , v ) {
if ( k === '$$date' ) { return new Date ( v ) ; }
if ( typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null ) { return v ; }
if ( v && v . $$date ) { return v . $$date ; }
return v ;
} ) ;
}
/ * *
* Deep copy a DB object
* The optional strictKeys flag ( defaulting to false ) indicates whether to copy everything or only fields
* where the keys are valid , i . e . don 't begin with $ and don' t contain a .
* /
function deepCopy ( obj , strictKeys ) {
var res ;
if ( typeof obj === 'boolean' ||
typeof obj === 'number' ||
typeof obj === 'string' ||
obj === null ||
( util . isDate ( obj ) ) ) {
return obj ;
}
if ( util . isArray ( obj ) ) {
res = [ ] ;
obj . forEach ( function ( o ) { res . push ( deepCopy ( o , strictKeys ) ) ; } ) ;
return res ;
}
if ( typeof obj === 'object' ) {
res = { } ;
Object . keys ( obj ) . forEach ( function ( k ) {
if ( ! strictKeys || ( k [ 0 ] !== '$' && k . indexOf ( '.' ) === - 1 ) ) {
res [ k ] = deepCopy ( obj [ k ] , strictKeys ) ;
}
} ) ;
return res ;
}
return undefined ; // For now everything else is undefined. We should probably throw an error instead
}
/ * *
* Tells if an object is a primitive type or a "real" object
* Arrays are considered primitive
* /
function isPrimitiveType ( obj ) {
return ( typeof obj === 'boolean' ||
typeof obj === 'number' ||
typeof obj === 'string' ||
obj === null ||
util . isDate ( obj ) ||
util . isArray ( obj ) ) ;
}
/ * *
* Utility functions for comparing things
* Assumes type checking was already done ( a and b already have the same type )
* compareNSB works for numbers , strings and booleans
* /
function compareNSB ( a , b ) {
if ( a < b ) { return - 1 ; }
if ( a > b ) { return 1 ; }
return 0 ;
}
function compareArrays ( a , b ) {
var i , comp ;
for ( i = 0 ; i < Math . min ( a . length , b . length ) ; i += 1 ) {
comp = compareThings ( a [ i ] , b [ i ] ) ;
if ( comp !== 0 ) { return comp ; }
}
// Common section was identical, longest one wins
return compareNSB ( a . length , b . length ) ;
}
/ * *
* Compare { things U undefined }
* Things are defined as any native types ( string , number , boolean , null , date ) and objects
* We need to compare with undefined as it will be used in indexes
* In the case of objects and arrays , we deep - compare
* If two objects dont have the same type , the ( arbitrary ) type hierarchy is : undefined , null , number , strings , boolean , dates , arrays , objects
* Return - 1 if a < b , 1 if a > b and 0 if a = b ( note that equality here is NOT the same as defined in areThingsEqual ! )
*
* @ param { Function } _compareStrings String comparing function , returning - 1 , 0 or 1 , overriding default string comparison ( useful for languages with accented letters )
* /
function compareThings ( a , b , _compareStrings ) {
var aKeys , bKeys , comp , i
, compareStrings = _compareStrings || compareNSB ;
// undefined
if ( a === undefined ) { return b === undefined ? 0 : - 1 ; }
if ( b === undefined ) { return a === undefined ? 0 : 1 ; }
// null
if ( a === null ) { return b === null ? 0 : - 1 ; }
if ( b === null ) { return a === null ? 0 : 1 ; }
// Numbers
if ( typeof a === 'number' ) { return typeof b === 'number' ? compareNSB ( a , b ) : - 1 ; }
if ( typeof b === 'number' ) { return typeof a === 'number' ? compareNSB ( a , b ) : 1 ; }
// Strings
if ( typeof a === 'string' ) { return typeof b === 'string' ? compareStrings ( a , b ) : - 1 ; }
if ( typeof b === 'string' ) { return typeof a === 'string' ? compareStrings ( a , b ) : 1 ; }
// Booleans
if ( typeof a === 'boolean' ) { return typeof b === 'boolean' ? compareNSB ( a , b ) : - 1 ; }
if ( typeof b === 'boolean' ) { return typeof a === 'boolean' ? compareNSB ( a , b ) : 1 ; }
// Dates
if ( util . isDate ( a ) ) { return util . isDate ( b ) ? compareNSB ( a . getTime ( ) , b . getTime ( ) ) : - 1 ; }
if ( util . isDate ( b ) ) { return util . isDate ( a ) ? compareNSB ( a . getTime ( ) , b . getTime ( ) ) : 1 ; }
// Arrays (first element is most significant and so on)
if ( util . isArray ( a ) ) { return util . isArray ( b ) ? compareArrays ( a , b ) : - 1 ; }
if ( util . isArray ( b ) ) { return util . isArray ( a ) ? compareArrays ( a , b ) : 1 ; }
// Objects
aKeys = Object . keys ( a ) . sort ( ) ;
bKeys = Object . keys ( b ) . sort ( ) ;
for ( i = 0 ; i < Math . min ( aKeys . length , bKeys . length ) ; i += 1 ) {
comp = compareThings ( a [ aKeys [ i ] ] , b [ bKeys [ i ] ] ) ;
if ( comp !== 0 ) { return comp ; }
}
return compareNSB ( aKeys . length , bKeys . length ) ;
}
// ==============================================================
// Updating documents
// ==============================================================
/ * *
* The signature of modifier functions is as follows
* Their structure is always the same : recursively follow the dot notation while creating
* the nested documents if needed , then apply the "last step modifier"
* @ param { Object } obj The model to modify
* @ param { String } field Can contain dots , in that case that means we will set a subfield recursively
* @ param { Model } value
* /
/ * *
* Set a field to a new value
* /
lastStepModifierFunctions . $set = function ( obj , field , value ) {
obj [ field ] = value ;
} ;
/ * *
* Unset a field
* /
lastStepModifierFunctions . $unset = function ( obj , field , value ) {
delete obj [ field ] ;
} ;
/ * *
* Push an element to the end of an array field
* Optional modifier $each instead of value to push several values
* Optional modifier $slice to slice the resulting array , see https : //docs.mongodb.org/manual/reference/operator/update/slice/
* Différeence with MongoDB : if $slice is specified and not $each , we act as if value is an empty array
* /
lastStepModifierFunctions . $push = function ( obj , field , value ) {
// Create the array if it doesn't exist
if ( ! obj . hasOwnProperty ( field ) ) { obj [ field ] = [ ] ; }
if ( ! util . isArray ( obj [ field ] ) ) { throw new Error ( "Can't $push an element on non-array values" ) ; }
if ( value !== null && typeof value === 'object' && value . $slice && value . $each === undefined ) {
value . $each = [ ] ;
}
if ( value !== null && typeof value === 'object' && value . $each ) {
if ( Object . keys ( value ) . length >= 3 || ( Object . keys ( value ) . length === 2 && value . $slice === undefined ) ) { throw new Error ( "Can only use $slice in cunjunction with $each when $push to array" ) ; }
if ( ! util . isArray ( value . $each ) ) { throw new Error ( "$each requires an array value" ) ; }
value . $each . forEach ( function ( v ) {
obj [ field ] . push ( v ) ;
} ) ;
if ( value . $slice === undefined || typeof value . $slice !== 'number' ) { return ; }
if ( value . $slice === 0 ) {
obj [ field ] = [ ] ;
} else {
var start , end , n = obj [ field ] . length ;
if ( value . $slice < 0 ) {
start = Math . max ( 0 , n + value . $slice ) ;
end = n ;
} else if ( value . $slice > 0 ) {
start = 0 ;
end = Math . min ( n , value . $slice ) ;
}
obj [ field ] = obj [ field ] . slice ( start , end ) ;
}
} else {
obj [ field ] . push ( value ) ;
}
} ;
/ * *
* Add an element to an array field only if it is not already in it
* No modification if the element is already in the array
* Note that it doesn ' t check whether the original array contains duplicates
* /
lastStepModifierFunctions . $addToSet = function ( obj , field , value ) {
var addToSet = true ;
// Create the array if it doesn't exist
if ( ! obj . hasOwnProperty ( field ) ) { obj [ field ] = [ ] ; }
if ( ! util . isArray ( obj [ field ] ) ) { throw new Error ( "Can't $addToSet an element on non-array values" ) ; }
if ( value !== null && typeof value === 'object' && value . $each ) {
if ( Object . keys ( value ) . length > 1 ) { throw new Error ( "Can't use another field in conjunction with $each" ) ; }
if ( ! util . isArray ( value . $each ) ) { throw new Error ( "$each requires an array value" ) ; }
value . $each . forEach ( function ( v ) {
lastStepModifierFunctions . $addToSet ( obj , field , v ) ;
} ) ;
} else {
obj [ field ] . forEach ( function ( v ) {
if ( compareThings ( v , value ) === 0 ) { addToSet = false ; }
} ) ;
if ( addToSet ) { obj [ field ] . push ( value ) ; }
}
} ;
/ * *
* Remove the first or last element of an array
* /
lastStepModifierFunctions . $pop = function ( obj , field , value ) {
if ( ! util . isArray ( obj [ field ] ) ) { throw new Error ( "Can't $pop an element from non-array values" ) ; }
if ( typeof value !== 'number' ) { throw new Error ( value + " isn't an integer, can't use it with $pop" ) ; }
if ( value === 0 ) { return ; }
if ( value > 0 ) {
obj [ field ] = obj [ field ] . slice ( 0 , obj [ field ] . length - 1 ) ;
} else {
obj [ field ] = obj [ field ] . slice ( 1 ) ;
}
} ;
/ * *
* Removes all instances of a value from an existing array
* /
lastStepModifierFunctions . $pull = function ( obj , field , value ) {
var arr , i ;
if ( ! util . isArray ( obj [ field ] ) ) { throw new Error ( "Can't $pull an element from non-array values" ) ; }
arr = obj [ field ] ;
for ( i = arr . length - 1 ; i >= 0 ; i -= 1 ) {
if ( match ( arr [ i ] , value ) ) {
arr . splice ( i , 1 ) ;
}
}
} ;
/ * *
* Increment a numeric field ' s value
* /
lastStepModifierFunctions . $inc = function ( obj , field , value ) {
if ( typeof value !== 'number' ) { throw new Error ( value + " must be a number" ) ; }
if ( typeof obj [ field ] !== 'number' ) {
if ( ! _ . has ( obj , field ) ) {
obj [ field ] = value ;
} else {
throw new Error ( "Don't use the $inc modifier on non-number fields" ) ;
}
} else {
obj [ field ] += value ;
}
} ;
/ * *
* Updates the value of the field , only if specified field is greater than the current value of the field
* /
lastStepModifierFunctions . $max = function ( obj , field , value ) {
if ( typeof obj [ field ] === 'undefined' ) {
obj [ field ] = value ;
} else if ( value > obj [ field ] ) {
obj [ field ] = value ;
}
} ;
/ * *
* Updates the value of the field , only if specified field is smaller than the current value of the field
* /
lastStepModifierFunctions . $min = function ( obj , field , value ) {
if ( typeof obj [ field ] === 'undefined' ) {
obj [ field ] = value ;
} else if ( value < obj [ field ] ) {
obj [ field ] = value ;
}
} ;
// Given its name, create the complete modifier function
function createModifierFunction ( modifier ) {
return function ( obj , field , value ) {
var fieldParts = typeof field === 'string' ? field . split ( '.' ) : field ;
if ( fieldParts . length === 1 ) {
lastStepModifierFunctions [ modifier ] ( obj , field , value ) ;
} else {
obj [ fieldParts [ 0 ] ] = obj [ fieldParts [ 0 ] ] || { } ;
modifierFunctions [ modifier ] ( obj [ fieldParts [ 0 ] ] , fieldParts . slice ( 1 ) , value ) ;
}
} ;
}
// Actually create all modifier functions
Object . keys ( lastStepModifierFunctions ) . forEach ( function ( modifier ) {
modifierFunctions [ modifier ] = createModifierFunction ( modifier ) ;
} ) ;
/ * *
* Modify a DB object according to an update query
* /
function modify ( obj , updateQuery ) {
var keys = Object . keys ( updateQuery )
, firstChars = _ . map ( keys , function ( item ) { return item [ 0 ] ; } )
, dollarFirstChars = _ . filter ( firstChars , function ( c ) { return c === '$' ; } )
, newDoc , modifiers
;
if ( keys . indexOf ( '_id' ) !== - 1 && updateQuery . _id !== obj . _id ) { throw new Error ( "You cannot change a document's _id" ) ; }
if ( dollarFirstChars . length !== 0 && dollarFirstChars . length !== firstChars . length ) {
throw new Error ( "You cannot mix modifiers and normal fields" ) ;
}
if ( dollarFirstChars . length === 0 ) {
// Simply replace the object with the update query contents
newDoc = deepCopy ( updateQuery ) ;
newDoc . _id = obj . _id ;
} else {
// Apply modifiers
modifiers = _ . uniq ( keys ) ;
newDoc = deepCopy ( obj ) ;
modifiers . forEach ( function ( m ) {
var keys ;
if ( ! modifierFunctions [ m ] ) { throw new Error ( "Unknown modifier " + m ) ; }
// Can't rely on Object.keys throwing on non objects since ES6{
// Not 100% satisfying as non objects can be interpreted as objects but no false negatives so we can live with it
if ( typeof updateQuery [ m ] !== 'object' ) {
throw new Error ( "Modifier " + m + "'s argument must be an object" ) ;
}
keys = Object . keys ( updateQuery [ m ] ) ;
keys . forEach ( function ( k ) {
modifierFunctions [ m ] ( newDoc , k , updateQuery [ m ] [ k ] ) ;
} ) ;
} ) ;
}
// Check result is valid and return it
checkObject ( newDoc ) ;
if ( obj . _id !== newDoc . _id ) { throw new Error ( "You can't change a document's _id" ) ; }
return newDoc ;
} ;
// ==============================================================
// 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
, i , objs ;
if ( ! obj ) { return undefined ; } // field cannot be empty so that means we should return undefined so that nothing can match
if ( fieldParts . length === 0 ) { return obj ; }
if ( fieldParts . length === 1 ) { return obj [ fieldParts [ 0 ] ] ; }
if ( util . isArray ( obj [ fieldParts [ 0 ] ] ) ) {
// If the next field is an integer, return only this item of the array
i = parseInt ( fieldParts [ 1 ] , 10 ) ;
if ( typeof i === 'number' && ! isNaN ( i ) ) {
return getDotValue ( obj [ fieldParts [ 0 ] ] [ i ] , fieldParts . slice ( 2 ) )
}
// Return the array of values
objs = new Array ( ) ;
for ( i = 0 ; i < obj [ fieldParts [ 0 ] ] . length ; i += 1 ) {
objs . push ( getDotValue ( obj [ fieldParts [ 0 ] ] [ i ] , fieldParts . slice ( 1 ) ) ) ;
}
return objs ;
} 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
* In the case of object , we check deep equality
* Returns true if they are , false otherwise
* /
function areThingsEqual ( a , b ) {
var aKeys , bKeys , i ;
// Strings, booleans, numbers, null
if ( a === null || typeof a === 'string' || typeof a === 'boolean' || typeof a === 'number' ||
b === null || typeof b === 'string' || typeof b === 'boolean' || typeof b === 'number' ) { return a === b ; }
// Dates
if ( util . isDate ( a ) || util . isDate ( b ) ) { return util . isDate ( a ) && util . isDate ( b ) && a . getTime ( ) === b . getTime ( ) ; }
// Arrays (no match since arrays are used as a $in)
// undefined (no match since they mean field doesn't exist and can't be serialized)
if ( ( ! ( util . isArray ( a ) && util . isArray ( b ) ) && ( util . isArray ( a ) || util . isArray ( b ) ) ) || a === undefined || b === undefined ) { return false ; }
// General objects (check for deep equality)
// a and b should be objects at this point
try {
aKeys = Object . keys ( a ) ;
bKeys = Object . keys ( b ) ;
} catch ( e ) {
return false ;
}
if ( aKeys . length !== bKeys . length ) { return false ; }
for ( i = 0 ; i < aKeys . length ; i += 1 ) {
if ( bKeys . indexOf ( aKeys [ i ] ) === - 1 ) { return false ; }
if ( ! areThingsEqual ( a [ aKeys [ i ] ] , b [ aKeys [ i ] ] ) ) { return false ; }
}
return true ;
}
/ * *
* Check that two values are comparable
* /
function areComparable ( a , b ) {
if ( typeof a !== 'string' && typeof a !== 'number' && ! util . isDate ( a ) &&
typeof b !== 'string' && typeof b !== 'number' && ! util . isDate ( b ) ) {
return false ;
}
if ( typeof a !== typeof b ) { return false ; }
return true ;
}
/ * *
* Arithmetic and comparison operators
* @ param { Native value } a Value in the object
* @ param { Native value } b Value in the query
* /
comparisonFunctions . $lt = function ( a , b ) {
return areComparable ( a , b ) && a < b ;
} ;
comparisonFunctions . $lte = function ( a , b ) {
return areComparable ( a , b ) && a <= b ;
} ;
comparisonFunctions . $gt = function ( a , b ) {
return areComparable ( a , b ) && a > b ;
} ;
comparisonFunctions . $gte = function ( a , b ) {
return areComparable ( a , b ) && a >= b ;
} ;
comparisonFunctions . $ne = function ( a , b ) {
if ( a === undefined ) { return true ; }
return ! areThingsEqual ( a , b ) ;
} ;
comparisonFunctions . $in = function ( a , b ) {
var i ;
if ( ! util . isArray ( b ) ) { throw new Error ( "$in operator called with a non-array" ) ; }
for ( i = 0 ; i < b . length ; i += 1 ) {
if ( areThingsEqual ( a , b [ i ] ) ) { return true ; }
}
return false ;
} ;
comparisonFunctions . $nin = function ( a , b ) {
if ( ! util . isArray ( b ) ) { throw new Error ( "$nin operator called with a non-array" ) ; }
return ! comparisonFunctions . $in ( a , b ) ;
} ;
comparisonFunctions . $regex = function ( a , b ) {
if ( ! util . isRegExp ( b ) ) { throw new Error ( "$regex operator called with non regular expression" ) ; }
if ( typeof a !== 'string' ) {
return false
} else {
return b . test ( a ) ;
}
} ;
comparisonFunctions . $exists = function ( value , exists ) {
if ( exists || exists === '' ) { // This will be true for all values of exists except false, null, undefined and 0
exists = true ; // That's strange behaviour (we should only use true/false) but that's the way Mongo does it...
} else {
exists = false ;
}
if ( value === undefined ) {
return ! exists
} else {
return exists ;
}
} ;
// Specific to arrays
comparisonFunctions . $size = function ( obj , value ) {
if ( ! util . isArray ( obj ) ) { return false ; }
if ( value % 1 !== 0 ) { throw new Error ( "$size operator called without an integer" ) ; }
return ( obj . length == value ) ;
} ;
comparisonFunctions . $elemMatch = function ( obj , value ) {
if ( ! util . isArray ( obj ) ) { return false ; }
var i = obj . length ;
var result = false ; // Initialize result
while ( i -- ) {
if ( match ( obj [ i ] , value ) ) { // If match for array element, return true
result = true ;
break ;
}
}
return result ;
} ;
arrayComparisonFunctions . $size = true ;
arrayComparisonFunctions . $elemMatch = true ;
/ * *
* Match any of the subqueries
* @ param { Model } obj
* @ param { Array of Queries } query
* /
logicalOperators . $or = function ( obj , query ) {
var i ;
if ( ! util . isArray ( query ) ) { throw new Error ( "$or operator used without an array" ) ; }
for ( i = 0 ; i < query . length ; i += 1 ) {
if ( match ( obj , query [ i ] ) ) { return true ; }
}
return false ;
} ;
/ * *
* Match all of the subqueries
* @ param { Model } obj
* @ param { Array of Queries } query
* /
logicalOperators . $and = function ( obj , query ) {
var i ;
if ( ! util . isArray ( query ) ) { throw new Error ( "$and operator used without an array" ) ; }
for ( i = 0 ; i < query . length ; i += 1 ) {
if ( ! match ( obj , query [ i ] ) ) { return false ; }
}
return true ;
} ;
/ * *
* Inverted match of the query
* @ param { Model } obj
* @ param { Query } query
* /
logicalOperators . $not = function ( obj , query ) {
return ! match ( obj , query ) ;
} ;
/ * *
* Use a function to match
* @ param { Model } obj
* @ param { Query } query
* /
logicalOperators . $where = function ( obj , fn ) {
var result ;
if ( ! _ . isFunction ( fn ) ) { throw new Error ( "$where operator used without a function" ) ; }
result = fn . call ( obj ) ;
if ( ! _ . isBoolean ( result ) ) { throw new Error ( "$where function must return boolean" ) ; }
return result ;
} ;
/ * *
* Tell if a given document matches a query
* @ param { Object } obj Document to check
* @ param { Object } query
* /
function match ( obj , query ) {
var queryKeys , queryKey , queryValue , i ;
// Primitive query against a primitive type
// This is a bit of a hack since we construct an object with an arbitrary key only to dereference it later
// But I don't have time for a cleaner implementation now
if ( isPrimitiveType ( obj ) || isPrimitiveType ( query ) ) {
return matchQueryPart ( { needAKey : obj } , 'needAKey' , query ) ;
}
// Normal query
queryKeys = Object . keys ( query ) ;
for ( i = 0 ; i < queryKeys . length ; i += 1 ) {
queryKey = queryKeys [ i ] ;
queryValue = query [ queryKey ] ;
if ( queryKey [ 0 ] === '$' ) {
if ( ! logicalOperators [ queryKey ] ) { throw new Error ( "Unknown logical operator " + queryKey ) ; }
if ( ! logicalOperators [ queryKey ] ( obj , queryValue ) ) { return false ; }
} else {
if ( ! matchQueryPart ( obj , queryKey , queryValue ) ) { return false ; }
}
}
return true ;
} ;
/ * *
* Match an object against a specific { key : value } part of a query
* if the treatObjAsValue flag is set , don ' t try to match every part separately , but the array as a whole
* /
function matchQueryPart ( obj , queryKey , queryValue , treatObjAsValue ) {
var objValue = getDotValue ( obj , queryKey )
, i , keys , firstChars , dollarFirstChars ;
// Check if the value is an array if we don't force a treatment as value
if ( util . isArray ( objValue ) && ! treatObjAsValue ) {
// If the queryValue is an array, try to perform an exact match
if ( util . isArray ( queryValue ) ) {
return matchQueryPart ( obj , queryKey , queryValue , true ) ;
}
// Check if we are using an array-specific comparison function
if ( queryValue !== null && typeof queryValue === 'object' && ! util . isRegExp ( queryValue ) ) {
keys = Object . keys ( queryValue ) ;
for ( i = 0 ; i < keys . length ; i += 1 ) {
if ( arrayComparisonFunctions [ keys [ i ] ] ) { return matchQueryPart ( obj , queryKey , queryValue , true ) ; }
}
}
// If not, treat it as an array of { obj, query } where there needs to be at least one match
for ( i = 0 ; i < objValue . length ; i += 1 ) {
if ( matchQueryPart ( { k : objValue [ i ] } , 'k' , queryValue ) ) { return true ; } // k here could be any string
}
return false ;
}
// 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' && ! util . isRegExp ( queryValue ) && ! util . isArray ( queryValue ) ) {
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 new Error ( "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 new Error ( "Unknown comparison function " + keys [ i ] ) ; }
if ( ! comparisonFunctions [ keys [ i ] ] ( objValue , queryValue [ keys [ i ] ] ) ) { return false ; }
}
return true ;
}
}
// Using regular expressions with basic querying
if ( util . isRegExp ( queryValue ) ) { return comparisonFunctions . $regex ( objValue , queryValue ) ; }
// queryValue is either a native value or a normal object
// Basic matching is possible
if ( ! areThingsEqual ( objValue , queryValue ) ) { return false ; }
return true ;
}
// Interface
module . exports . serialize = serialize ;
module . exports . deserialize = deserialize ;
module . exports . deepCopy = deepCopy ;
module . exports . checkObject = checkObject ;
module . exports . isPrimitiveType = isPrimitiveType ;
module . exports . modify = modify ;
module . exports . getDotValue = getDotValue ;
module . exports . match = match ;
module . exports . areThingsEqual = areThingsEqual ;
module . exports . compareThings = compareThings ;