@ -20,36 +20,38 @@ const arrayComparisonFunctions = {}
* 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 ( )
}
const 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 [ 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 .' )
}
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 ) {
const checkObject = obj => {
if ( Array . isArray ( obj ) ) {
obj . forEach ( function ( o ) {
obj . forEach ( o => {
checkObject ( o )
} )
}
if ( typeof obj === 'object' && obj !== null ) {
Object . keys ( obj ) . forEach ( function ( k ) {
for ( const k in obj ) {
if ( Object . prototype . hasOwnProperty . call ( obj , k ) ) {
checkKey ( k , obj [ k ] )
checkObject ( obj [ k ] )
} )
}
}
}
}
@ -61,36 +63,37 @@ function checkObject (obj) {
* Accepted primitive types : Number , String , Boolean , Date , null
* Accepted secondary types : Objects , Arrays
* /
function serialize ( obj ) {
const res = JSON . stringify ( obj , function ( k , v ) {
const serialize = obj => {
return JSON . stringify ( obj , function ( k , v ) {
checkKey ( k , v )
if ( v === undefined ) { return undefined }
if ( v === null ) { return null }
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 ( ) } }
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 }
const deserialize = rawData => 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
@ -98,29 +101,26 @@ function deserialize (rawData) {
* where the keys are valid , i . e . don 't begin with $ and don' t contain a .
* /
function deepCopy ( obj , strictKeys ) {
let res
if ( typeof obj === 'boolean' ||
if (
typeof obj === 'boolean' ||
typeof obj === 'number' ||
typeof obj === 'string' ||
obj === null ||
( util . types . isDate ( obj ) ) ) {
return obj
}
( util . types . isDate ( obj ) )
) return obj
if ( Array . isArray ( obj ) ) {
res = [ ]
obj . forEach ( function ( o ) { res . push ( deepCopy ( o , strictKeys ) ) } )
return res
}
if ( Array . isArray ( obj ) ) return obj . map ( o => deepCopy ( o , strictKeys ) )
if ( typeof obj === 'object' ) {
res = { }
Object . keys ( obj ) . forEach ( function ( k ) {
if ( ! strictKeys || ( k [ 0 ] !== '$' && k . indexOf ( '.' ) === - 1 ) ) {
const res = { }
for ( const k in obj ) {
if (
Object . prototype . hasOwnProperty . call ( obj , k ) &&
( ! strictKeys || ( k [ 0 ] !== '$' && k . indexOf ( '.' ) === - 1 ) )
) {
res [ k ] = deepCopy ( obj [ k ] , strictKeys )
}
} )
}
return res
}
@ -131,34 +131,32 @@ function deepCopy (obj, strictKeys) {
* Tells if an object is a primitive type or a "real" object
* Arrays are considered primitive
* /
function isPrimitiveType ( obj ) {
return ( typeof obj === 'boolean' ||
const isPrimitiveType = obj => (
typeof obj === 'boolean' ||
typeof obj === 'number' ||
typeof obj === 'string' ||
obj === null ||
util . types . isDate ( obj ) ||
Array . isArray ( obj ) )
}
Array . 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 }
const compareNSB = ( a , b ) => {
if ( a < b ) return - 1
if ( a > b ) return 1
return 0
}
function compareArrays ( a , b ) {
let i
let comp
for ( i = 0 ; i < Math . min ( a . length , b . length ) ; i += 1 ) {
comp = compareThings ( a [ i ] , b [ i ] )
const compareArrays = ( a , b ) => {
const minLength = Math . min ( a . length , b . length )
for ( let i = 0 ; i < minLength ; i += 1 ) {
const comp = compareThings ( a [ i ] , b [ i ] )
if ( comp !== 0 ) { return comp }
if ( comp !== 0 ) return comp
}
// Common section was identical, longest one wins
@ -175,47 +173,45 @@ function compareArrays (a, b) {
*
* @ 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 ) {
let comp
let i
const compareThings = ( a , b , _compareStrings ) => {
const compareStrings = _compareStrings || compareNSB
// undefined
if ( a === undefined ) { return b === undefined ? 0 : - 1 }
if ( b === undefined ) { return a === undefined ? 0 : 1 }
if ( a === undefined ) return b === undefined ? 0 : - 1
if ( b === undefined ) return 1 // no need to test if a === undefined
// null
if ( a === null ) { return b === null ? 0 : - 1 }
if ( b === null ) { return a === null ? 0 : 1 }
if ( a === null ) return b === null ? 0 : - 1
if ( b === null ) return 1 // no need to test if a === null
// 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 }
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 }
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 }
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 . types . isDate ( a ) ) { return util . types . isDate ( b ) ? compareNSB ( a . getTime ( ) , b . getTime ( ) ) : - 1 }
if ( util . types . isDate ( b ) ) { return util . types . isDate ( a ) ? compareNSB ( a . getTime ( ) , b . getTime ( ) ) : 1 }
if ( util . types . isDate ( a ) ) return util . types . isDate ( b ) ? compareNSB ( a . getTime ( ) , b . getTime ( ) ) : - 1
if ( util . types . isDate ( b ) ) return util . types . isDate ( a ) ? compareNSB ( a . getTime ( ) , b . getTime ( ) ) : 1
// Arrays (first element is most significant and so on)
if ( Array . isArray ( a ) ) { return Array . isArray ( b ) ? compareArrays ( a , b ) : - 1 }
if ( Array . isArray ( b ) ) { return Array . isArray ( a ) ? compareArrays ( a , b ) : 1 }
if ( Array . isArray ( a ) ) return Array . isArray ( b ) ? compareArrays ( a , b ) : - 1
if ( Array . isArray ( b ) ) return Array . isArray ( a ) ? compareArrays ( a , b ) : 1
// Objects
const aKeys = Object . keys ( a ) . sort ( )
const 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 ] ] )
for ( let i = 0 ; i < Math . min ( aKeys . length , bKeys . length ) ; i += 1 ) {
const comp = compareThings ( a [ aKeys [ i ] ] , b [ bKeys [ i ] ] )
if ( comp !== 0 ) { return comp }
if ( comp !== 0 ) return comp
}
return compareNSB ( aKeys . length , bKeys . length )
@ -237,14 +233,14 @@ function compareThings (a, b, _compareStrings) {
/ * *
* Set a field to a new value
* /
lastStepModifierFunctions . $set = function ( obj , field , value ) {
lastStepModifierFunctions . $set = ( obj , field , value ) => {
obj [ field ] = value
}
/ * *
* Unset a field
* /
lastStepModifierFunctions . $unset = function ( obj , field , value ) {
lastStepModifierFunctions . $unset = ( obj , field , value ) => {
delete obj [ field ]
}
@ -254,29 +250,34 @@ lastStepModifierFunctions.$unset = function (obj, field, value) {
* 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 ) {
lastStepModifierFunctions . $push = ( obj , field , value ) => {
// Create the array if it doesn't exist
if ( ! Object . prototype . hasOwnProperty . call ( obj , field ) ) { obj [ field ] = [ ] }
if ( ! Object . prototype . hasOwnProperty . call ( obj , field ) ) obj [ field ] = [ ]
if ( ! Array . isArray ( obj [ field ] ) ) { throw new Error ( 'Can\'t $push an element on non-array values' ) }
if ( ! Array . 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 . $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 ( ! Array . isArray ( value . $each ) ) { throw new Error ( '$each requires an array value' ) }
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 ( ! Array . isArray ( value . $each ) ) throw new Error ( '$each requires an array value' )
value . $each . forEach ( function ( v ) {
value . $each . forEach ( v => {
obj [ field ] . push ( v )
} )
if ( value . $slice === undefined || typeof value . $slice !== 'number' ) { return }
if ( value . $slice === undefined || typeof value . $slice !== 'number' ) return
if ( value . $slice === 0 ) {
obj [ field ] = [ ]
} else {
if ( value . $slice === 0 ) obj [ field ] = [ ]
else {
let start
let end
const n = obj [ field ] . length
@ -299,134 +300,112 @@ lastStepModifierFunctions.$push = function (obj, field, value) {
* 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 ) {
let addToSet = true
lastStepModifierFunctions . $addToSet = ( obj , field , value ) => {
// Create the array if it doesn't exist
if ( ! Object . prototype . hasOwnProperty . call ( obj , field ) ) { obj [ field ] = [ ] }
if ( ! Array . isArray ( obj [ field ] ) ) { throw new Error ( 'Can\'t $addToSet an element on non-array values' ) }
if ( ! Array . 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 ( ! Array . isArray ( value . $each ) ) { throw new Error ( '$each requires an array value' ) }
if ( Object . keys ( value ) . length > 1 ) throw new Error ( 'Can\'t use another field in conjunction with $each' )
if ( ! Array . isArray ( value . $each ) ) throw new Error ( '$each requires an array value' )
value . $each . forEach ( function ( v ) {
value . $each . forEach ( v => {
lastStepModifierFunctions . $addToSet ( obj , field , v )
} )
} else {
obj [ field ] . forEach ( function ( v ) {
if ( compareThings ( v , value ) === 0 ) { addToSet = false }
let addToSet = true
obj [ field ] . forEach ( v => {
if ( compareThings ( v , value ) === 0 ) addToSet = false
} )
if ( addToSet ) { obj [ field ] . push ( value ) }
if ( addToSet ) obj [ field ] . push ( value )
}
}
/ * *
* Remove the first or last element of an array
* /
lastStepModifierFunctions . $pop = function ( obj , field , value ) {
if ( ! Array . 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 }
lastStepModifierFunctions . $pop = ( obj , field , value ) => {
if ( ! Array . 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 )
}
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 ) {
if ( ! Array . isArray ( obj [ field ] ) ) { throw new Error ( 'Can\'t $pull an element from non-array values' ) }
lastStepModifierFunctions . $pull = ( obj , field , value ) => {
if ( ! Array . isArray ( obj [ field ] ) ) throw new Error ( 'Can\'t $pull an element from non-array values' )
const arr = obj [ field ]
for ( let i = arr . length - 1 ; i >= 0 ; i -= 1 ) {
if ( match ( arr [ i ] , value ) ) {
arr . splice ( 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' ) }
lastStepModifierFunctions . $inc = ( obj , field , value ) => {
if ( typeof value !== 'number' ) throw new Error ( ` ${ value } must be a number ` )
if ( typeof obj [ field ] !== 'number' ) {
if ( ! Object . prototype . hasOwnProperty . call ( obj , field ) ) {
obj [ field ] = value
} else {
throw new Error ( 'Don\'t use the $inc modifier on non-number fields' )
}
} else {
obj [ field ] += value
}
if ( ! Object . prototype . hasOwnProperty . call ( 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
}
lastStepModifierFunctions . $max = ( 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
}
lastStepModifierFunctions . $min = ( 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 ) {
const createModifierFunction = modifier => ( obj , field , value ) => {
const fieldParts = typeof field === 'string' ? field . split ( '.' ) : field
if ( fieldParts . length === 1 ) {
lastStepModifierFunctions [ modifier ] ( obj , field , value )
} else {
if ( fieldParts . length === 1 ) lastStepModifierFunctions [ modifier ] ( obj , field , value )
else {
if ( obj [ fieldParts [ 0 ] ] === undefined ) {
if ( modifier === '$unset' ) { return } // Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented
if ( modifier === '$unset' ) return // Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented
obj [ fieldParts [ 0 ] ] = { }
}
modifierFunctions [ modifier ] ( obj [ fieldParts [ 0 ] ] , fieldParts . slice ( 1 ) , value )
}
}
}
// Actually create all modifier functions
Object . keys ( lastStepModifierFunctions ) . forEach ( function ( modifier ) {
Object . keys ( lastStepModifierFunctions ) . forEach ( modifier => {
modifierFunctions [ modifier ] = createModifierFunction ( modifier )
} )
/ * *
* Modify a DB object according to an update query
* /
function modify ( obj , updateQuery ) {
const modify = ( obj , updateQuery ) => {
const keys = Object . keys ( updateQuery )
const firstChars = keys . map ( item => item [ 0 ] )
const dollarFirstChars = firstChars . filter ( c => c === '$' )
let newDoc
let modifiers
if ( keys . indexOf ( '_id' ) !== - 1 && updateQuery . _id !== obj . _id ) { throw new Error ( 'You cannot change a document\'s _id' ) }
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 && 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
@ -436,17 +415,15 @@ function modify (obj, updateQuery) {
// Apply modifiers
modifiers = uniq ( keys )
newDoc = deepCopy ( obj )
modifiers . forEach ( function ( m ) {
if ( ! modifierFunctions [ m ] ) { throw new Error ( 'Unknown modifier ' + m ) }
modifiers . forEach ( m => {
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' )
}
if ( typeof updateQuery [ m ] !== 'object' ) throw new Error ( ` Modifier ${ m } 's argument must be an object ` )
const keys = Object . keys ( updateQuery [ m ] )
keys . forEach ( function ( k ) {
keys . forEach ( k => {
modifierFunctions [ m ] ( newDoc , k , updateQuery [ m ] [ k ] )
} )
} )
@ -455,7 +432,7 @@ function modify (obj, updateQuery) {
// 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' ) }
if ( obj . _id !== newDoc . _id ) throw new Error ( 'You can\'t change a document\'s _id' )
return newDoc
}
@ -468,33 +445,23 @@ function modify (obj, updateQuery) {
* @ param { Object } obj
* @ param { String } field
* /
function getDotValue ( obj , field ) {
const getDotValue = ( obj , field ) => {
const fieldParts = typeof field === 'string' ? field . split ( '.' ) : field
let i
let objs
if ( ! obj ) { return undefined } // field cannot be empty so that means we should return undefined so that nothing can match
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 === 0 ) return obj
if ( fieldParts . length === 1 ) { return obj [ fieldParts [ 0 ] ] }
if ( fieldParts . length === 1 ) return obj [ fieldParts [ 0 ] ]
if ( Array . 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 ) )
}
const 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 = [ ]
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 ) )
}
return obj [ fieldParts [ 0 ] ] . map ( el => getDotValue ( el , fieldParts . slice ( 1 ) ) )
} else return getDotValue ( obj [ fieldParts [ 0 ] ] , fieldParts . slice ( 1 ) )
}
/ * *
@ -503,24 +470,33 @@ function getDotValue (obj, field) {
* In the case of object , we check deep equality
* Returns true if they are , false otherwise
* /
function areThingsEqual ( a , b ) {
let aKeys
let bKeys
let i
const areThingsEqual = ( a , b ) => {
// 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 }
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 . types . isDate ( a ) || util . types . isDate ( b ) ) { return util . types . isDate ( a ) && util . types . isDate ( b ) && a . getTime ( ) === b . getTime ( ) }
if ( util . types . isDate ( a ) || util . types . isDate ( b ) ) return util . types . isDate ( a ) && util . types . 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 ( ( ! ( Array . isArray ( a ) && Array . isArray ( b ) ) && ( Array . isArray ( a ) || Array . isArray ( b ) ) ) || a === undefined || b === undefined ) { return false }
if (
( ! ( Array . isArray ( a ) && Array . isArray ( b ) ) && ( Array . isArray ( a ) || Array . isArray ( b ) ) ) ||
a === undefined || b === undefined
) return false
// General objects (check for deep equality)
// a and b should be objects at this point
let aKeys
let bKeys
try {
aKeys = Object . keys ( a )
bKeys = Object . keys ( b )
@ -528,10 +504,10 @@ function areThingsEqual (a, b) {
return false
}
if ( aKeys . length !== bKeys . length ) { return false }
for ( i = 0 ; i < aKeys . length ; i += 1 ) {
if ( bKeys . indexOf ( aK eys [ i ] ) === - 1 ) { return false }
if ( ! areThingsEqual ( a [ aK eys [ i ] ] , b [ aK eys [ i ] ] ) ) { return false }
if ( aKeys . length !== bKeys . length ) return false
for ( const el of aKeys ) {
if ( bKeys . indexOf ( el ) === - 1 ) return false
if ( ! areThingsEqual ( a [ el ] , b [ el ] ) ) return false
}
return true
}
@ -539,13 +515,17 @@ function areThingsEqual (a, b) {
/ * *
* Check that two values are comparable
* /
function areComparable ( a , b ) {
if ( typeof a !== 'string' && typeof a !== 'number' && ! util . types . isDate ( a ) &&
typeof b !== 'string' && typeof b !== 'number' && ! util . types . isDate ( b ) ) {
return false
}
if ( typeof a !== typeof b ) { return false }
const areComparable = ( a , b ) => {
if (
typeof a !== 'string' &&
typeof a !== 'number' &&
! util . types . isDate ( a ) &&
typeof b !== 'string' &&
typeof b !== 'number' &&
! util . types . isDate ( b )
) return false
if ( typeof a !== typeof b ) return false
return true
}
@ -555,88 +535,62 @@ function areComparable (a, b) {
* @ 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 . $lt = ( a , b ) => areComparable ( a , b ) && a < b
comparisonFunctions . $lte = function ( a , b ) {
return areComparable ( a , b ) && a <= b
}
comparisonFunctions . $lte = ( a , b ) => areComparable ( a , b ) && a <= b
comparisonFunctions . $gt = function ( a , b ) {
return areComparable ( a , b ) && a > b
}
comparisonFunctions . $gt = ( a , b ) => areComparable ( a , b ) && a > b
comparisonFunctions . $gte = function ( a , b ) {
return areComparable ( a , b ) && a >= b
}
comparisonFunctions . $gte = ( a , b ) => areComparable ( a , b ) && a >= b
comparisonFunctions . $ne = function ( a , b ) {
if ( a === undefined ) { return true }
return ! areThingsEqual ( a , b )
}
comparisonFunctions . $ne = ( a , b ) => a === undefined || ! areThingsEqual ( a , b )
comparisonFunctions . $in = function ( a , b ) {
let i
comparisonFunctions . $in = ( a , b ) => {
if ( ! Array . isArray ( b ) ) throw new Error ( '$in operator called with a non-array' )
if ( ! Array . 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 }
for ( const el of b ) {
if ( areThingsEqual ( a , el ) ) return true
}
return false
}
comparisonFunctions . $nin = function ( a , b ) {
if ( ! Array . isArray ( b ) ) { throw new Error ( '$nin operator called with a non-array' ) }
comparisonFunctions . $nin = ( a , b ) => {
if ( ! Array . isArray ( b ) ) throw new Error ( '$nin operator called with a non-array' )
return ! comparisonFunctions . $in ( a , b )
}
comparisonFunctions . $regex = function ( a , b ) {
if ( ! util . types . isRegExp ( b ) ) { throw new Error ( '$regex operator called with non regular expression' ) }
comparisonFunctions . $regex = ( a , b ) => {
if ( ! util . types . isRegExp ( b ) ) throw new Error ( '$regex operator called with non regular expression' )
if ( typeof a !== 'string' ) {
return false
} else {
return b . test ( a )
}
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 stat 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
}
comparisonFunctions . $exists = ( value , exists ) => {
// This will be true for all values of stat except false, null, undefined and 0
// That's strange behaviour (we should only use true/false) but that's the way Mongo does it...
if ( exists || exists === '' ) exists = true
else exists = false
if ( value === undefined ) {
return ! exists
} else {
return exists
}
if ( value === undefined ) return ! exists
else return exists
}
// Specific to arrays
comparisonFunctions . $size = function ( obj , value ) {
if ( ! Array . isArray ( obj ) ) { return false }
if ( value % 1 !== 0 ) { throw new Error ( '$size operator called without an integer' ) }
comparisonFunctions . $size = ( obj , value ) => {
if ( ! Array . 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 ( ! Array . isArray ( obj ) ) { return false }
let i = obj . length
let result = false // Initialize result
while ( i -- ) {
if ( match ( obj [ i ] , value ) ) { // If match for array element, return true
result = true
break
}
}
return result
comparisonFunctions . $elemMatch = ( obj , value ) => {
if ( ! Array . isArray ( obj ) ) return false
return obj . some ( el => match ( el , value ) )
}
arrayComparisonFunctions . $size = true
arrayComparisonFunctions . $elemMatch = true
@ -645,13 +599,11 @@ arrayComparisonFunctions.$elemMatch = true
* @ param { Model } obj
* @ param { Array of Queries } query
* /
logicalOperators . $or = function ( obj , query ) {
let i
if ( ! Array . isArray ( query ) ) { throw new Error ( '$or operator used without an array' ) }
logicalOperators . $or = ( obj , query ) => {
if ( ! Array . 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 }
for ( let i = 0 ; i < query . length ; i += 1 ) {
if ( match ( obj , query [ i ] ) ) return true
}
return false
@ -662,13 +614,11 @@ logicalOperators.$or = function (obj, query) {
* @ param { Model } obj
* @ param { Array of Queries } query
* /
logicalOperators . $and = function ( obj , query ) {
let i
logicalOperators . $and = ( obj , query ) => {
if ( ! Array . isArray ( query ) ) throw new Error ( '$and operator used without an array' )
if ( ! Array . 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 }
for ( let i = 0 ; i < query . length ; i += 1 ) {
if ( ! match ( obj , query [ i ] ) ) return false
}
return true
@ -679,20 +629,18 @@ logicalOperators.$and = function (obj, query) {
* @ param { Model } obj
* @ param { Query } query
* /
logicalOperators . $not = function ( obj , query ) {
return ! match ( obj , query )
}
logicalOperators . $not = ( obj , query ) => ! match ( obj , query )
/ * *
* Use a function to match
* @ param { Model } obj
* @ param { Query } query
* /
logicalOperators . $where = function ( obj , fn ) {
if ( typeof fn !== 'function' ) { throw new Error ( '$where operator used without a function' ) }
logicalOperators . $where = ( obj , fn ) => {
if ( typeof fn !== 'function' ) throw new Error ( '$where operator used without a function' )
const result = fn . call ( obj )
if ( typeof result !== 'boolean' ) { throw new Error ( '$where function must return boolean' ) }
if ( typeof result !== 'boolean' ) throw new Error ( '$where function must return boolean' )
return result
}
@ -702,29 +650,20 @@ logicalOperators.$where = function (obj, fn) {
* @ param { Object } obj Document to check
* @ param { Object } query
* /
function match ( obj , query ) {
let queryKey
let queryValue
let i
const match = ( obj , query ) => {
// 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 )
}
if ( isPrimitiveType ( obj ) || isPrimitiveType ( query ) ) return matchQueryPart ( { needAKey : obj } , 'needAKey' , query )
// Normal query
const queryKeys = Object . keys ( query )
for ( i = 0 ; i < queryKeys . length ; i += 1 ) {
queryKey = queryKeys [ i ]
queryValue = query [ queryKey ]
for ( const queryKey in query ) {
if ( Object . prototype . hasOwnProperty . call ( query , queryKey ) ) {
const 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 }
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
}
}
@ -737,29 +676,22 @@ function match (obj, query) {
* /
function matchQueryPart ( obj , queryKey , queryValue , treatObjAsValue ) {
const objValue = getDotValue ( obj , queryKey )
let i
let keys
let firstChars
let dollarFirstChars
// Check if the value is an array if we don't force a treatment as value
if ( Array . isArray ( objValue ) && ! treatObjAsValue ) {
// If the queryValue is an array, try to perform an exact match
if ( Array . isArray ( queryValue ) ) {
return matchQueryPart ( obj , queryKey , queryValue , true )
}
if ( Array . 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 . types . isRegExp ( queryValue ) ) {
keys = Object . keys ( queryValue )
for ( i = 0 ; i < keys . length ; i += 1 ) {
if ( arrayComparisonFunctions [ keys [ i ] ] ) { return matchQueryPart ( obj , queryKey , queryValue , true ) }
for ( const key in queryValue ) {
if ( Object . prototype . hasOwnProperty . call ( queryValue , key ) && arrayComparisonFunctions [ key ] ) { 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 : objValu e[ i ] } , 'k' , queryValue ) ) { return true } // k here could be any string
for ( const el of objValue ) {
if ( matchQueryPart ( { k : el } , 'k' , queryValue ) ) return true // k here could be any string
}
return false
}
@ -767,33 +699,29 @@ function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) {
// 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 . types . isRegExp ( queryValue ) && ! Array . isArray ( queryValue ) ) {
keys = Object . keys ( queryValue )
firstChars = keys . map ( item => item [ 0 ] )
dollarFirstChars = firstChars . filter ( c => c === '$' )
const keys = Object . keys ( queryValue )
const firstChars = keys . map ( item => item [ 0 ] )
const dollarFirstChars = firstChars . filter ( c => c === '$' )
if ( dollarFirstChars . length !== 0 && dollarFirstChars . length !== firstChars . length ) {
throw new Error ( 'You cannot mix operators and normal fields' )
}
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 ] ) }
for ( const key of keys ) {
if ( ! comparisonFunctions [ key ] ) throw new Error ( ` Unknown comparison function ${ key } ` )
if ( ! comparisonFunctions [ keys [ i ] ] ( objValue , queryValue [ keys [ i ] ] ) ) { return false }
if ( ! comparisonFunctions [ key ] ( objValue , queryValue [ key ] ) ) return false
}
return true
}
}
// Using regular expressions with basic querying
if ( util . types . isRegExp ( queryValue ) ) { return comparisonFunctions . $regex ( objValue , queryValue ) }
if ( util . types . 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
return areThingsEqual ( objValue , queryValue )
}
// Interface