|
|
@ -3,19 +3,9 @@ |
|
|
|
* Serialization/deserialization |
|
|
|
* Serialization/deserialization |
|
|
|
* Copying |
|
|
|
* Copying |
|
|
|
* Querying, update |
|
|
|
* Querying, update |
|
|
|
|
|
|
|
* @module model |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const { uniq, isDate, isRegExp } = require('./utils.js') |
|
|
|
const { uniq, isDate, isRegExp } = require('./utils.js') |
|
|
|
/** |
|
|
|
|
|
|
|
* @type {Object.<string, Model~modifierFunction>} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
const modifierFunctions = {} |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* @type {Object.<string, Model~modifierFunction>} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
const lastStepModifierFunctions = {} |
|
|
|
|
|
|
|
const comparisonFunctions = {} |
|
|
|
|
|
|
|
const logicalOperators = {} |
|
|
|
|
|
|
|
const arrayComparisonFunctions = {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Check a key, throw an error if the key is non valid |
|
|
|
* Check a key, throw an error if the key is non valid |
|
|
@ -24,6 +14,7 @@ const arrayComparisonFunctions = {} |
|
|
|
* Non-treatable edge cases here: if part of the object if of the form { $$date: number } or { $$deleted: true } |
|
|
|
* 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 |
|
|
|
* 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... |
|
|
|
* But you really need to want it to trigger such behaviour, even when warned not to use '$' at the beginning of the field names... |
|
|
|
|
|
|
|
* @private |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const checkKey = (k, v) => { |
|
|
|
const checkKey = (k, v) => { |
|
|
|
if (typeof k === 'number') k = k.toString() |
|
|
|
if (typeof k === 'number') k = k.toString() |
|
|
@ -43,6 +34,7 @@ const checkKey = (k, v) => { |
|
|
|
* Check a DB object and throw an error if it's not valid |
|
|
|
* Check a DB object and throw an error if it's not valid |
|
|
|
* Works by applying the above checkKey function to all fields recursively |
|
|
|
* Works by applying the above checkKey function to all fields recursively |
|
|
|
* @param {document|document[]} obj |
|
|
|
* @param {document|document[]} obj |
|
|
|
|
|
|
|
* @alias module:model.checkObject |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const checkObject = obj => { |
|
|
|
const checkObject = obj => { |
|
|
|
if (Array.isArray(obj)) { |
|
|
|
if (Array.isArray(obj)) { |
|
|
@ -70,6 +62,7 @@ const checkObject = obj => { |
|
|
|
* Accepted secondary types: Objects, Arrays |
|
|
|
* Accepted secondary types: Objects, Arrays |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @return {string} |
|
|
|
* @return {string} |
|
|
|
|
|
|
|
* @alias module:model.serialize |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const serialize = obj => { |
|
|
|
const serialize = obj => { |
|
|
|
return JSON.stringify(obj, function (k, v) { |
|
|
|
return JSON.stringify(obj, function (k, v) { |
|
|
@ -91,6 +84,7 @@ const serialize = obj => { |
|
|
|
* Return the object itself |
|
|
|
* Return the object itself |
|
|
|
* @param {string} rawData |
|
|
|
* @param {string} rawData |
|
|
|
* @return {document} |
|
|
|
* @return {document} |
|
|
|
|
|
|
|
* @alias module:model.deserialize |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const deserialize = rawData => JSON.parse(rawData, function (k, v) { |
|
|
|
const deserialize = rawData => JSON.parse(rawData, function (k, v) { |
|
|
|
if (k === '$$date') return new Date(v) |
|
|
|
if (k === '$$date') return new Date(v) |
|
|
@ -112,6 +106,7 @@ const deserialize = rawData => JSON.parse(rawData, function (k, v) { |
|
|
|
* @param {?document} obj |
|
|
|
* @param {?document} obj |
|
|
|
* @param {boolean} [strictKeys=false] |
|
|
|
* @param {boolean} [strictKeys=false] |
|
|
|
* @return {?document} |
|
|
|
* @return {?document} |
|
|
|
|
|
|
|
* @alias module:modelel:(.*) |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
function deepCopy (obj, strictKeys) { |
|
|
|
function deepCopy (obj, strictKeys) { |
|
|
|
if ( |
|
|
|
if ( |
|
|
@ -145,6 +140,7 @@ function deepCopy (obj, strictKeys) { |
|
|
|
* Arrays are considered primitive |
|
|
|
* Arrays are considered primitive |
|
|
|
* @param {*} obj |
|
|
|
* @param {*} obj |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
* @alias module:modelel:(.*) |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const isPrimitiveType = obj => ( |
|
|
|
const isPrimitiveType = obj => ( |
|
|
|
typeof obj === 'boolean' || |
|
|
|
typeof obj === 'boolean' || |
|
|
@ -162,6 +158,7 @@ const isPrimitiveType = obj => ( |
|
|
|
* @param {number|string|boolean} a |
|
|
|
* @param {number|string|boolean} a |
|
|
|
* @param {number|string|boolean} b |
|
|
|
* @param {number|string|boolean} b |
|
|
|
* @return {number} 0 if a == b, 1 i a > b, -1 if a < b |
|
|
|
* @return {number} 0 if a == b, 1 i a > b, -1 if a < b |
|
|
|
|
|
|
|
* @private |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const compareNSB = (a, b) => { |
|
|
|
const compareNSB = (a, b) => { |
|
|
|
if (a < b) return -1 |
|
|
|
if (a < b) return -1 |
|
|
@ -176,6 +173,7 @@ const compareNSB = (a, b) => { |
|
|
|
* @param {Array} a |
|
|
|
* @param {Array} a |
|
|
|
* @param {Array} b |
|
|
|
* @param {Array} b |
|
|
|
* @return {number} 0 if arrays have the same length and all elements equal one another. Else either 1 or -1. |
|
|
|
* @return {number} 0 if arrays have the same length and all elements equal one another. Else either 1 or -1. |
|
|
|
|
|
|
|
* @private |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const compareArrays = (a, b) => { |
|
|
|
const compareArrays = (a, b) => { |
|
|
|
const minLength = Math.min(a.length, b.length) |
|
|
|
const minLength = Math.min(a.length, b.length) |
|
|
@ -198,8 +196,9 @@ const compareArrays = (a, b) => { |
|
|
|
* 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!) |
|
|
|
* 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 {*} a |
|
|
|
* @param {*} a |
|
|
|
* @param {*} b |
|
|
|
* @param {*} b |
|
|
|
* @param {Function} [_compareStrings] String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters) |
|
|
|
* @param {compareStrings} [_compareStrings] String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters) |
|
|
|
* @return {number} |
|
|
|
* @return {number} |
|
|
|
|
|
|
|
* @alias module:model.compareThings |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const compareThings = (a, b, _compareStrings) => { |
|
|
|
const compareThings = (a, b, _compareStrings) => { |
|
|
|
const compareStrings = _compareStrings || compareNSB |
|
|
|
const compareStrings = _compareStrings || compareNSB |
|
|
@ -250,7 +249,7 @@ const compareThings = (a, b, _compareStrings) => { |
|
|
|
// ==============================================================
|
|
|
|
// ==============================================================
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* @callback Model~modifierFunction |
|
|
|
* @callback modifierFunction |
|
|
|
* The signature of modifier functions is as follows |
|
|
|
* The signature of modifier functions is as follows |
|
|
|
* Their structure is always the same: recursively follow the dot notation while creating |
|
|
|
* Their structure is always the same: recursively follow the dot notation while creating |
|
|
|
* the nested documents if needed, then apply the "last step modifier" |
|
|
|
* the nested documents if needed, then apply the "last step modifier" |
|
|
@ -260,29 +259,126 @@ const compareThings = (a, b, _compareStrings) => { |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Set a field to a new value |
|
|
|
* Create the complete modifier function |
|
|
|
* @type Model~modifierFunction |
|
|
|
* @param {function} lastStepModifierFunction a lastStepModifierFunction |
|
|
|
|
|
|
|
* @param {boolean} [unset = false] Bad looking specific fix, needs to be generalized modifiers that behave like $unset are implemented |
|
|
|
|
|
|
|
* @return {modifierFunction} |
|
|
|
|
|
|
|
* @private |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
lastStepModifierFunctions.$set = (obj, field, value) => { |
|
|
|
const createModifierFunction = (lastStepModifierFunction, unset = false) => (obj, field, value) => { |
|
|
|
obj[field] = value |
|
|
|
const func = (obj, field, value) => { |
|
|
|
|
|
|
|
const fieldParts = typeof field === 'string' ? field.split('.') : field |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldParts.length === 1) lastStepModifierFunction(obj, field, value) |
|
|
|
|
|
|
|
else { |
|
|
|
|
|
|
|
if (obj[fieldParts[0]] === undefined) { |
|
|
|
|
|
|
|
if (unset) return |
|
|
|
|
|
|
|
obj[fieldParts[0]] = {} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
func(obj[fieldParts[0]], fieldParts.slice(1), value) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return func(obj, field, value) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const $addToSetPartial = (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 (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') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
value.$each.forEach(v => { |
|
|
|
|
|
|
|
$addToSetPartial(obj, field, v) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
let addToSet = true |
|
|
|
|
|
|
|
obj[field].forEach(v => { |
|
|
|
|
|
|
|
if (compareThings(v, value) === 0) addToSet = false |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
if (addToSet) obj[field].push(value) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
|
|
|
|
* @enum {modifierFunction} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
const modifierFunctions = { |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Set a field to a new value |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$set: createModifierFunction((obj, field, value) => { |
|
|
|
|
|
|
|
obj[field] = value |
|
|
|
|
|
|
|
}), |
|
|
|
|
|
|
|
/** |
|
|
|
* Unset a field |
|
|
|
* Unset a field |
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
lastStepModifierFunctions.$unset = (obj, field, value) => { |
|
|
|
$unset: createModifierFunction((obj, field, value) => { |
|
|
|
delete obj[field] |
|
|
|
delete obj[field] |
|
|
|
} |
|
|
|
}, true), |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Updates the value of the field, only if specified field is smaller than the current value of the field |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$min: createModifierFunction((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 greater than the current value of the field |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$max: createModifierFunction((obj, field, value) => { |
|
|
|
|
|
|
|
if (typeof obj[field] === 'undefined') obj[field] = value |
|
|
|
|
|
|
|
else if (value > obj[field]) obj[field] = value |
|
|
|
|
|
|
|
}), |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Increment a numeric field's value |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$inc: createModifierFunction((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 |
|
|
|
|
|
|
|
}), |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Removes all instances of a value from an existing array |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$pull: createModifierFunction((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) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}), |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Remove the first or last element of an array |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$pop: createModifierFunction((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) |
|
|
|
|
|
|
|
}), |
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 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 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
$addToSet: createModifierFunction($addToSetPartial), |
|
|
|
|
|
|
|
/** |
|
|
|
* Push an element to the end of an array field |
|
|
|
* Push an element to the end of an array field |
|
|
|
* Optional modifier $each instead of value to push several values |
|
|
|
* 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/
|
|
|
|
* 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 |
|
|
|
* Difference with MongoDB: if $slice is specified and not $each, we act as if value is an empty array |
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
lastStepModifierFunctions.$push = (obj, field, value) => { |
|
|
|
$push: createModifierFunction((obj, field, value) => { |
|
|
|
// Create the array if it doesn't exist
|
|
|
|
// 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] = [] |
|
|
|
|
|
|
|
|
|
|
@ -325,121 +421,16 @@ lastStepModifierFunctions.$push = (obj, field, value) => { |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
obj[field].push(value) |
|
|
|
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 |
|
|
|
|
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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 (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') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
value.$each.forEach(v => { |
|
|
|
|
|
|
|
lastStepModifierFunctions.$addToSet(obj, field, v) |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
} else { |
|
|
|
|
|
|
|
let addToSet = true |
|
|
|
|
|
|
|
obj[field].forEach(v => { |
|
|
|
|
|
|
|
if (compareThings(v, value) === 0) addToSet = false |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
if (addToSet) obj[field].push(value) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Remove the first or last element of an array |
|
|
|
|
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Removes all instances of a value from an existing array |
|
|
|
|
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Increment a numeric field's value |
|
|
|
|
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Updates the value of the field, only if specified field is greater than the current value of the field |
|
|
|
|
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
* @type Model~modifierFunction |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
lastStepModifierFunctions.$min = (obj, field, value) => { |
|
|
|
|
|
|
|
if (typeof obj[field] === 'undefined') obj[field] = value |
|
|
|
|
|
|
|
else if (value < obj[field]) obj[field] = value |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Create the complete modifier function |
|
|
|
|
|
|
|
* @param {string} modifier one of lastStepModifierFunctions keys |
|
|
|
|
|
|
|
* @return {Model~modifierFunction} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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 (obj[fieldParts[0]] === undefined) { |
|
|
|
|
|
|
|
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(modifier => { |
|
|
|
|
|
|
|
modifierFunctions[modifier] = createModifierFunction(modifier) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Modify a DB object according to an update query |
|
|
|
* Modify a DB object according to an update query |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @param {query} updateQuery |
|
|
|
* @param {query} updateQuery |
|
|
|
* @return {document} |
|
|
|
* @return {document} |
|
|
|
|
|
|
|
* @alias module:model.modify |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const modify = (obj, updateQuery) => { |
|
|
|
const modify = (obj, updateQuery) => { |
|
|
|
const keys = Object.keys(updateQuery) |
|
|
|
const keys = Object.keys(updateQuery) |
|
|
@ -490,6 +481,7 @@ const modify = (obj, updateQuery) => { |
|
|
|
* @param {object} obj |
|
|
|
* @param {object} obj |
|
|
|
* @param {string} field |
|
|
|
* @param {string} field |
|
|
|
* @return {*} |
|
|
|
* @return {*} |
|
|
|
|
|
|
|
* @alias module:model.getDotValue |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const getDotValue = (obj, field) => { |
|
|
|
const getDotValue = (obj, field) => { |
|
|
|
const fieldParts = typeof field === 'string' ? field.split('.') : field |
|
|
|
const fieldParts = typeof field === 'string' ? field.split('.') : field |
|
|
@ -518,6 +510,7 @@ const getDotValue = (obj, field) => { |
|
|
|
* @param {*} a |
|
|
|
* @param {*} a |
|
|
|
* @param {*} a |
|
|
|
* @param {*} a |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
* @alias module:model.areThingsEqual |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const areThingsEqual = (a, b) => { |
|
|
|
const areThingsEqual = (a, b) => { |
|
|
|
// Strings, booleans, numbers, null
|
|
|
|
// Strings, booleans, numbers, null
|
|
|
@ -566,6 +559,7 @@ const areThingsEqual = (a, b) => { |
|
|
|
* @param {*} a |
|
|
|
* @param {*} a |
|
|
|
* @param {*} a |
|
|
|
* @param {*} a |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
* @private |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const areComparable = (a, b) => { |
|
|
|
const areComparable = (a, b) => { |
|
|
|
if ( |
|
|
|
if ( |
|
|
@ -583,7 +577,7 @@ const areComparable = (a, b) => { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* @callback Model~comparisonOperator |
|
|
|
* @callback comparisonOperator |
|
|
|
* Arithmetic and comparison operators |
|
|
|
* Arithmetic and comparison operators |
|
|
|
* @param {*} a Value in the object |
|
|
|
* @param {*} a Value in the object |
|
|
|
* @param {*} b Value in the query |
|
|
|
* @param {*} b Value in the query |
|
|
@ -591,40 +585,21 @@ const areComparable = (a, b) => { |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Lower than |
|
|
|
* @enum {comparisonOperator} |
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$lt = (a, b) => areComparable(a, b) && a < b |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Lower than or equals |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$lte = (a, b) => areComparable(a, b) && a <= b |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Greater than |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$gt = (a, b) => areComparable(a, b) && a > b |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Greater than or equals |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$gte = (a, b) => areComparable(a, b) && a >= b |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Does not equal |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
*/ |
|
|
|
comparisonFunctions.$ne = (a, b) => a === undefined || !areThingsEqual(a, b) |
|
|
|
const comparisonFunctions = { |
|
|
|
|
|
|
|
/** Lower than */ |
|
|
|
/** |
|
|
|
$lt: (a, b) => areComparable(a, b) && a < b, |
|
|
|
* Is in Array |
|
|
|
/** Lower than or equals */ |
|
|
|
* @type Model~comparisonOperator |
|
|
|
$lte: (a, b) => areComparable(a, b) && a <= b, |
|
|
|
*/ |
|
|
|
/** Greater than */ |
|
|
|
comparisonFunctions.$in = (a, b) => { |
|
|
|
$gt: (a, b) => areComparable(a, b) && a > b, |
|
|
|
|
|
|
|
/** Greater than or equals */ |
|
|
|
|
|
|
|
$gte: (a, b) => areComparable(a, b) && a >= b, |
|
|
|
|
|
|
|
/** Does not equal */ |
|
|
|
|
|
|
|
$ne: (a, b) => a === undefined || !areThingsEqual(a, b), |
|
|
|
|
|
|
|
/** Is in Array */ |
|
|
|
|
|
|
|
$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 (const el of b) { |
|
|
|
for (const el of b) { |
|
|
@ -632,33 +607,22 @@ comparisonFunctions.$in = (a, b) => { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false |
|
|
|
return false |
|
|
|
} |
|
|
|
}, |
|
|
|
/** |
|
|
|
/** Is not in Array */ |
|
|
|
* Is not in Array |
|
|
|
$nin: (a, b) => { |
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$nin = (a, b) => { |
|
|
|
|
|
|
|
if (!Array.isArray(b)) throw new Error('$nin operator called with a non-array') |
|
|
|
if (!Array.isArray(b)) throw new Error('$nin operator called with a non-array') |
|
|
|
|
|
|
|
|
|
|
|
return !comparisonFunctions.$in(a, b) |
|
|
|
return !comparisonFunctions.$in(a, b) |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** Matches Regexp */ |
|
|
|
/** |
|
|
|
$regex: (a, b) => { |
|
|
|
* Matches Regexp |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$regex = (a, b) => { |
|
|
|
|
|
|
|
if (!isRegExp(b)) throw new Error('$regex operator called with non regular expression') |
|
|
|
if (!isRegExp(b)) throw new Error('$regex operator called with non regular expression') |
|
|
|
|
|
|
|
|
|
|
|
if (typeof a !== 'string') return false |
|
|
|
if (typeof a !== 'string') return false |
|
|
|
else return b.test(a) |
|
|
|
else return b.test(a) |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** Returns true if field exists */ |
|
|
|
/** |
|
|
|
$exists: (a, b) => { |
|
|
|
* Returns true if field exists |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$exists = (a, b) => { |
|
|
|
|
|
|
|
// This will be true for all values of stat except false, null, undefined and 0
|
|
|
|
// 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...
|
|
|
|
// That's strange behaviour (we should only use true/false) but that's the way Mongo does it...
|
|
|
|
if (b || b === '') b = true |
|
|
|
if (b || b === '') b = true |
|
|
@ -666,38 +630,34 @@ comparisonFunctions.$exists = (a, b) => { |
|
|
|
|
|
|
|
|
|
|
|
if (a === undefined) return !b |
|
|
|
if (a === undefined) return !b |
|
|
|
else return b |
|
|
|
else return b |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** Specific to Arrays, returns true if a length equals b */ |
|
|
|
/** |
|
|
|
$size: (a, b) => { |
|
|
|
* Specific to Arrays, returns true if a length equals b |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$size = (a, b) => { |
|
|
|
|
|
|
|
if (!Array.isArray(a)) return false |
|
|
|
if (!Array.isArray(a)) return false |
|
|
|
if (b % 1 !== 0) throw new Error('$size operator called without an integer') |
|
|
|
if (b % 1 !== 0) throw new Error('$size operator called without an integer') |
|
|
|
|
|
|
|
|
|
|
|
return a.length === b |
|
|
|
return a.length === b |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** Specific to Arrays, returns true if some elements of a match the query b */ |
|
|
|
/** |
|
|
|
$elemMatch: (a, b) => { |
|
|
|
* Specific to Arrays, returns true if some elements of a match the query b |
|
|
|
|
|
|
|
* @type Model~comparisonOperator |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
comparisonFunctions.$elemMatch = (a, b) => { |
|
|
|
|
|
|
|
if (!Array.isArray(a)) return false |
|
|
|
if (!Array.isArray(a)) return false |
|
|
|
return a.some(el => match(el, b)) |
|
|
|
return a.some(el => match(el, b)) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
arrayComparisonFunctions.$size = true |
|
|
|
const arrayComparisonFunctions = { $size: true, $elemMatch: true } |
|
|
|
arrayComparisonFunctions.$elemMatch = true |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
|
|
|
|
* @enum |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
const logicalOperators = { |
|
|
|
|
|
|
|
/** |
|
|
|
* Match any of the subqueries |
|
|
|
* Match any of the subqueries |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @param {query[]} query |
|
|
|
* @param {query[]} query |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
logicalOperators.$or = (obj, query) => { |
|
|
|
$or: (obj, query) => { |
|
|
|
if (!Array.isArray(query)) throw new Error('$or operator used without an array') |
|
|
|
if (!Array.isArray(query)) throw new Error('$or operator used without an array') |
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < query.length; i += 1) { |
|
|
|
for (let i = 0; i < query.length; i += 1) { |
|
|
@ -705,15 +665,14 @@ logicalOperators.$or = (obj, query) => { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false |
|
|
|
return false |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
|
|
|
|
* Match all of the subqueries |
|
|
|
* Match all of the subqueries |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @param {query[]} query |
|
|
|
* @param {query[]} query |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
logicalOperators.$and = (obj, query) => { |
|
|
|
$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 (let i = 0; i < query.length; i += 1) { |
|
|
|
for (let i = 0; i < query.length; i += 1) { |
|
|
@ -721,35 +680,35 @@ logicalOperators.$and = (obj, query) => { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return true |
|
|
|
return true |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
|
|
|
|
* Inverted match of the query |
|
|
|
* Inverted match of the query |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @param {query} query |
|
|
|
* @param {query} query |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
logicalOperators.$not = (obj, query) => !match(obj, query) |
|
|
|
$not: (obj, query) => !match(obj, query), |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* @callback Model~whereCallback |
|
|
|
* @callback whereCallback |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Use a function to match |
|
|
|
* Use a function to match |
|
|
|
* @param {document} obj |
|
|
|
* @param {document} obj |
|
|
|
* @param {Model~whereCallback} fn |
|
|
|
* @param {whereCallback} fn |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
logicalOperators.$where = (obj, fn) => { |
|
|
|
$where: (obj, fn) => { |
|
|
|
if (typeof fn !== 'function') throw new Error('$where operator used without a function') |
|
|
|
if (typeof fn !== 'function') throw new Error('$where operator used without a function') |
|
|
|
|
|
|
|
|
|
|
|
const result = fn.call(obj) |
|
|
|
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 |
|
|
|
return result |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
@ -757,6 +716,7 @@ logicalOperators.$where = (obj, fn) => { |
|
|
|
* @param {document} obj Document to check |
|
|
|
* @param {document} obj Document to check |
|
|
|
* @param {query} query |
|
|
|
* @param {query} query |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
* @alias module:model.match |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
const match = (obj, query) => { |
|
|
|
const match = (obj, query) => { |
|
|
|
// Primitive query against a primitive type
|
|
|
|
// Primitive query against a primitive type
|
|
|
@ -778,10 +738,6 @@ const match = (obj, query) => { |
|
|
|
return true |
|
|
|
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 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Match an object against a specific { key: value } part of a query |
|
|
|
* 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 |
|
|
|
* if the treatObjAsValue flag is set, don't try to match every part separately, but the array as a whole |
|
|
@ -790,6 +746,7 @@ const match = (obj, query) => { |
|
|
|
* @param {*} queryValue |
|
|
|
* @param {*} queryValue |
|
|
|
* @param {boolean} [treatObjAsValue=false] |
|
|
|
* @param {boolean} [treatObjAsValue=false] |
|
|
|
* @return {boolean} |
|
|
|
* @return {boolean} |
|
|
|
|
|
|
|
* @private |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) { |
|
|
|
function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) { |
|
|
|
const objValue = getDotValue(obj, queryKey) |
|
|
|
const objValue = getDotValue(obj, queryKey) |
|
|
|