From 0b8aab73468c7ca2dcaad4adb18e94bef31bdf5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Rebours?= Date: Wed, 5 Jan 2022 17:03:07 +0100 Subject: [PATCH] add doc to model, fix persistence doc, fix error in cursor and unecessary async in executor --- lib/cursor.js | 8 +- lib/executor.js | 2 +- lib/model.js | 186 ++++++++++++++++++++++++++++++++++++--------- lib/persistence.js | 14 ++-- 4 files changed, 164 insertions(+), 46 deletions(-) diff --git a/lib/cursor.js b/lib/cursor.js index 0882d88..8873cda 100755 --- a/lib/cursor.js +++ b/lib/cursor.js @@ -33,10 +33,10 @@ class Cursor { this.query = query || {} if (execFn) this.execFn = execFn if (async) this.async = true - this._limit = null - this._skip = null - this._sort = null - this._projection = null + this._limit = undefined + this._skip = undefined + this._sort = undefined + this._projection = undefined } /** diff --git a/lib/executor.js b/lib/executor.js index 872553f..e272ca1 100755 --- a/lib/executor.js +++ b/lib/executor.js @@ -38,7 +38,7 @@ class Waterfall { */ waterfall (func) { return (...args) => { - this._guardian = this.guardian.then(async () => { + this._guardian = this.guardian.then(() => { return func(...args) .then(result => ({ error: false, result }), result => ({ error: true, result })) }) diff --git a/lib/model.js b/lib/model.js index 3d24b96..2c6eeb1 100755 --- a/lib/model.js +++ b/lib/model.js @@ -5,7 +5,13 @@ * Querying, update */ const { uniq, isDate, isRegExp } = require('./utils.js') +/** + * @type {Object.} + */ const modifierFunctions = {} +/** + * @type {Object.} + */ const lastStepModifierFunctions = {} const comparisonFunctions = {} const logicalOperators = {} @@ -13,8 +19,8 @@ const 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 + * @param {string} k key + * @param {document} 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... @@ -36,6 +42,7 @@ const checkKey = (k, v) => { /** * Check a DB object and throw an error if it's not valid * Works by applying the above checkKey function to all fields recursively + * @param {document|document[]} obj */ const checkObject = obj => { if (Array.isArray(obj)) { @@ -61,6 +68,8 @@ const checkObject = obj => { * so eval and the like are not safe * Accepted primitive types: Number, String, Boolean, Date, null * Accepted secondary types: Objects, Arrays + * @param {document} obj + * @return {string} */ const serialize = obj => { return JSON.stringify(obj, function (k, v) { @@ -80,6 +89,8 @@ const serialize = obj => { /** * From a one-line representation of an object generate by the serialize function * Return the object itself + * @param {string} rawData + * @return {document} */ const deserialize = rawData => JSON.parse(rawData, function (k, v) { if (k === '$$date') return new Date(v) @@ -98,6 +109,9 @@ const deserialize = rawData => JSON.parse(rawData, function (k, 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 . + * @param {?document} obj + * @param {boolean} [strictKeys=false] + * @return {?document} */ function deepCopy (obj, strictKeys) { if ( @@ -129,6 +143,8 @@ function deepCopy (obj, strictKeys) { /** * Tells if an object is a primitive type or a "real" object * Arrays are considered primitive + * @param {*} obj + * @return {boolean} */ const isPrimitiveType = obj => ( typeof obj === 'boolean' || @@ -143,6 +159,9 @@ const isPrimitiveType = 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 + * @param {number|string|boolean} a + * @param {number|string|boolean} b + * @return {number} 0 if a == b, 1 i a > b, -1 if a < b */ const compareNSB = (a, b) => { if (a < b) return -1 @@ -150,6 +169,14 @@ const compareNSB = (a, b) => { return 0 } +/** + * Utility function for comparing array + * Assumes type checking was already done (a and b already have the same type) + * compareNSB works for numbers, strings and booleans + * @param {Array} a + * @param {Array} b + * @return {number} 0 if arrays have the same length and all elements equal one another. Else either 1 or -1. + */ const compareArrays = (a, b) => { const minLength = Math.min(a.length, b.length) for (let i = 0; i < minLength; i += 1) { @@ -169,8 +196,10 @@ const compareArrays = (a, b) => { * 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) + * @param {*} a + * @param {*} b + * @param {Function} [_compareStrings] String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters) + * @return {number} */ const compareThings = (a, b, _compareStrings) => { const compareStrings = _compareStrings || compareNSB @@ -221,16 +250,18 @@ const compareThings = (a, b, _compareStrings) => { // ============================================================== /** + * @callback Model~modifierFunction * 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 + * @param {document} value */ /** * Set a field to a new value + * @type Model~modifierFunction */ lastStepModifierFunctions.$set = (obj, field, value) => { obj[field] = value @@ -238,6 +269,7 @@ lastStepModifierFunctions.$set = (obj, field, value) => { /** * Unset a field + * @type Model~modifierFunction */ lastStepModifierFunctions.$unset = (obj, field, value) => { delete obj[field] @@ -248,6 +280,7 @@ lastStepModifierFunctions.$unset = (obj, field, value) => { * 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 + * @type Model~modifierFunction */ lastStepModifierFunctions.$push = (obj, field, value) => { // Create the array if it doesn't exist @@ -298,6 +331,7 @@ lastStepModifierFunctions.$push = (obj, field, 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 @@ -323,6 +357,7 @@ lastStepModifierFunctions.$addToSet = (obj, field, 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') @@ -335,6 +370,7 @@ lastStepModifierFunctions.$pop = (obj, field, value) => { /** * 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') @@ -347,6 +383,7 @@ lastStepModifierFunctions.$pull = (obj, field, value) => { /** * 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`) @@ -359,6 +396,7 @@ lastStepModifierFunctions.$inc = (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 @@ -367,13 +405,18 @@ lastStepModifierFunctions.$max = (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 } -// Given its name, create the complete modifier function +/** + * 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 @@ -394,6 +437,9 @@ Object.keys(lastStepModifierFunctions).forEach(modifier => { /** * Modify a DB object according to an update query + * @param {document} obj + * @param {query} updateQuery + * @return {document} */ const modify = (obj, updateQuery) => { const keys = Object.keys(updateQuery) @@ -441,8 +487,9 @@ const modify = (obj, updateQuery) => { /** * Get a value from object with dot notation - * @param {Object} obj - * @param {String} field + * @param {object} obj + * @param {string} field + * @return {*} */ const getDotValue = (obj, field) => { const fieldParts = typeof field === 'string' ? field.split('.') : field @@ -468,6 +515,9 @@ const getDotValue = (obj, field) => { * 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 + * @param {*} a + * @param {*} a + * @return {boolean} */ const areThingsEqual = (a, b) => { // Strings, booleans, numbers, null @@ -513,6 +563,9 @@ const areThingsEqual = (a, b) => { /** * Check that two values are comparable + * @param {*} a + * @param {*} a + * @return {boolean} */ const areComparable = (a, b) => { if ( @@ -530,20 +583,47 @@ const areComparable = (a, b) => { } /** + * @callback Model~comparisonOperator * Arithmetic and comparison operators - * @param {Native value} a Value in the object - * @param {Native value} b Value in the query + * @param {*} a Value in the object + * @param {*} b Value in the query + * @return {boolean} + */ + +/** + * Lower than + * @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) +/** + * Is in Array + * @type Model~comparisonOperator + */ comparisonFunctions.$in = (a, b) => { if (!Array.isArray(b)) throw new Error('$in operator called with a non-array') @@ -553,13 +633,20 @@ comparisonFunctions.$in = (a, b) => { return false } - +/** + * Is not in Array + * @type Model~comparisonOperator + */ comparisonFunctions.$nin = (a, b) => { if (!Array.isArray(b)) throw new Error('$nin operator called with a non-array') return !comparisonFunctions.$in(a, b) } +/** + * Matches Regexp + * @type Model~comparisonOperator + */ comparisonFunctions.$regex = (a, b) => { if (!isRegExp(b)) throw new Error('$regex operator called with non regular expression') @@ -567,27 +654,38 @@ comparisonFunctions.$regex = (a, b) => { else return b.test(a) } -comparisonFunctions.$exists = (value, exists) => { +/** + * 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 // 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 (b || b === '') b = true + else b = false - if (value === undefined) return !exists - else return exists + if (a === undefined) return !b + else return b } -// Specific to arrays -comparisonFunctions.$size = (obj, value) => { - if (!Array.isArray(obj)) return false - if (value % 1 !== 0) throw new Error('$size operator called without an integer') +/** + * Specific to Arrays, returns true if a length equals b + * @type Model~comparisonOperator + */ +comparisonFunctions.$size = (a, b) => { + if (!Array.isArray(a)) return false + if (b % 1 !== 0) throw new Error('$size operator called without an integer') - return obj.length === value + return a.length === b } -comparisonFunctions.$elemMatch = (obj, value) => { - if (!Array.isArray(obj)) return false - return obj.some(el => match(el, value)) +/** + * 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 + return a.some(el => match(el, b)) } arrayComparisonFunctions.$size = true @@ -595,8 +693,9 @@ arrayComparisonFunctions.$elemMatch = true /** * Match any of the subqueries - * @param {Model} obj - * @param {Array of Queries} query + * @param {document} obj + * @param {query[]} query + * @return {boolean} */ logicalOperators.$or = (obj, query) => { if (!Array.isArray(query)) throw new Error('$or operator used without an array') @@ -610,8 +709,9 @@ logicalOperators.$or = (obj, query) => { /** * Match all of the subqueries - * @param {Model} obj - * @param {Array of Queries} query + * @param {document} obj + * @param {query[]} query + * @return {boolean} */ logicalOperators.$and = (obj, query) => { if (!Array.isArray(query)) throw new Error('$and operator used without an array') @@ -625,15 +725,23 @@ logicalOperators.$and = (obj, query) => { /** * Inverted match of the query - * @param {Model} obj - * @param {Query} query + * @param {document} obj + * @param {query} query + * @return {boolean} */ logicalOperators.$not = (obj, query) => !match(obj, query) +/** + * @callback Model~whereCallback + * @param {document} obj + * @return {boolean} + */ + /** * Use a function to match - * @param {Model} obj - * @param {Query} query + * @param {document} obj + * @param {Model~whereCallback} fn + * @return {boolean} */ logicalOperators.$where = (obj, fn) => { if (typeof fn !== 'function') throw new Error('$where operator used without a function') @@ -646,8 +754,9 @@ logicalOperators.$where = (obj, fn) => { /** * Tell if a given document matches a query - * @param {Object} obj Document to check - * @param {Object} query + * @param {document} obj Document to check + * @param {query} query + * @return {boolean} */ const match = (obj, query) => { // Primitive query against a primitive type @@ -673,6 +782,15 @@ const match = (obj, 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 */ +/** + * 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 + * @param {object} obj + * @param {string} queryKey + * @param {*} queryValue + * @param {boolean} [treatObjAsValue=false] + * @return {boolean} + */ function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) { const objValue = getDotValue(obj, queryKey) diff --git a/lib/persistence.js b/lib/persistence.js index 93731c6..c7e0200 100755 --- a/lib/persistence.js +++ b/lib/persistence.js @@ -63,7 +63,7 @@ class Persistence { /** * @callback Persistence~persistCachedDatabaseCallback - * @param {Error?} err + * @param {?Error} err */ /** @@ -110,7 +110,7 @@ class Persistence { /** * @callback Persistence~compactDataFileCallback - * @param {Error?} err + * @param {?Error} err */ /** @@ -153,7 +153,7 @@ class Persistence { /** * @callback Persistence~persistNewStateCallback - * @param {Error?} err + * @param {?Error} err */ /** @@ -233,8 +233,8 @@ class Persistence { /** * @callback Persistence~treatRawStreamCallback - * @param {Error?} err - * @param {{data: document[], indexes: Object.}} data + * @param {?Error} err + * @param {{data: document[], indexes: Object.}?} data */ /** @@ -297,7 +297,7 @@ class Persistence { /** * @callback Persistence~loadDatabaseCallback - * @param {Error?} err + * @param {?Error} err */ /** @@ -361,7 +361,7 @@ class Persistence { /** * @callback Persistence~ensureDirectoryExistsCallback - * @param {Error?} err + * @param {?Error} err */ /**