add doc to model, fix persistence doc, fix error in cursor and unecessary async in executor

pull/11/head
Timothée Rebours 3 years ago
parent 90f52e4cda
commit 0b8aab7346
  1. 8
      lib/cursor.js
  2. 2
      lib/executor.js
  3. 186
      lib/model.js
  4. 14
      lib/persistence.js

@ -33,10 +33,10 @@ class Cursor {
this.query = query || {} this.query = query || {}
if (execFn) this.execFn = execFn if (execFn) this.execFn = execFn
if (async) this.async = true if (async) this.async = true
this._limit = null this._limit = undefined
this._skip = null this._skip = undefined
this._sort = null this._sort = undefined
this._projection = null this._projection = undefined
} }
/** /**

@ -38,7 +38,7 @@ class Waterfall {
*/ */
waterfall (func) { waterfall (func) {
return (...args) => { return (...args) => {
this._guardian = this.guardian.then(async () => { this._guardian = this.guardian.then(() => {
return func(...args) return func(...args)
.then(result => ({ error: false, result }), result => ({ error: true, result })) .then(result => ({ error: false, result }), result => ({ error: true, result }))
}) })

@ -5,7 +5,13 @@
* Querying, update * Querying, update
*/ */
const { uniq, isDate, isRegExp } = require('./utils.js') const { uniq, isDate, isRegExp } = require('./utils.js')
/**
* @type {Object.<string, Model~modifierFunction>}
*/
const modifierFunctions = {} const modifierFunctions = {}
/**
* @type {Object.<string, Model~modifierFunction>}
*/
const lastStepModifierFunctions = {} const lastStepModifierFunctions = {}
const comparisonFunctions = {} const comparisonFunctions = {}
const logicalOperators = {} const logicalOperators = {}
@ -13,8 +19,8 @@ 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
* @param {String} k key * @param {string} k key
* @param {Model} v value, needed to treat the Date edge case * @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 } * 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...
@ -36,6 +42,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
*/ */
const checkObject = obj => { const checkObject = obj => {
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
@ -61,6 +68,8 @@ const checkObject = obj => {
* so eval and the like are not safe * so eval and the like are not safe
* Accepted primitive types: Number, String, Boolean, Date, null * Accepted primitive types: Number, String, Boolean, Date, null
* Accepted secondary types: Objects, Arrays * Accepted secondary types: Objects, Arrays
* @param {document} obj
* @return {string}
*/ */
const serialize = obj => { const serialize = obj => {
return JSON.stringify(obj, function (k, v) { 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 * From a one-line representation of an object generate by the serialize function
* Return the object itself * Return the object itself
* @param {string} rawData
* @return {document}
*/ */
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)
@ -98,6 +109,9 @@ const deserialize = rawData => JSON.parse(rawData, function (k, v) {
* Deep copy a DB object * Deep copy a DB object
* The optional strictKeys flag (defaulting to false) indicates whether to copy everything or only fields * 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 . * 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) { function deepCopy (obj, strictKeys) {
if ( if (
@ -129,6 +143,8 @@ function deepCopy (obj, strictKeys) {
/** /**
* Tells if an object is a primitive type or a "real" object * Tells if an object is a primitive type or a "real" object
* Arrays are considered primitive * Arrays are considered primitive
* @param {*} obj
* @return {boolean}
*/ */
const isPrimitiveType = obj => ( const isPrimitiveType = obj => (
typeof obj === 'boolean' || typeof obj === 'boolean' ||
@ -143,6 +159,9 @@ const isPrimitiveType = obj => (
* Utility functions for comparing things * Utility functions for comparing things
* Assumes type checking was already done (a and b already have the same type) * Assumes type checking was already done (a and b already have the same type)
* compareNSB works for numbers, strings and booleans * 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) => { const compareNSB = (a, b) => {
if (a < b) return -1 if (a < b) return -1
@ -150,6 +169,14 @@ const compareNSB = (a, b) => {
return 0 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 compareArrays = (a, b) => {
const minLength = Math.min(a.length, b.length) const minLength = Math.min(a.length, b.length)
for (let i = 0; i < minLength; i += 1) { 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 * 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 * 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!) * 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 {Function} _compareStrings String comparing function, returning -1, 0 or 1, overriding default string comparison (useful for languages with accented letters) * @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 compareThings = (a, b, _compareStrings) => {
const compareStrings = _compareStrings || compareNSB const compareStrings = _compareStrings || compareNSB
@ -221,16 +250,18 @@ const compareThings = (a, b, _compareStrings) => {
// ============================================================== // ==============================================================
/** /**
* @callback Model~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"
* @param {Object} obj The model to modify * @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 {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 * Set a field to a new value
* @type Model~modifierFunction
*/ */
lastStepModifierFunctions.$set = (obj, field, value) => { lastStepModifierFunctions.$set = (obj, field, value) => {
obj[field] = value obj[field] = value
@ -238,6 +269,7 @@ lastStepModifierFunctions.$set = (obj, field, value) => {
/** /**
* Unset a field * Unset a field
* @type Model~modifierFunction
*/ */
lastStepModifierFunctions.$unset = (obj, field, value) => { lastStepModifierFunctions.$unset = (obj, field, value) => {
delete obj[field] delete obj[field]
@ -248,6 +280,7 @@ lastStepModifierFunctions.$unset = (obj, field, value) => {
* 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 * 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) => { lastStepModifierFunctions.$push = (obj, field, value) => {
// Create the array if it doesn't exist // 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 * 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 * No modification if the element is already in the array
* Note that it doesn't check whether the original array contains duplicates * Note that it doesn't check whether the original array contains duplicates
* @type Model~modifierFunction
*/ */
lastStepModifierFunctions.$addToSet = (obj, field, value) => { lastStepModifierFunctions.$addToSet = (obj, field, value) => {
// Create the array if it doesn't exist // 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 * Remove the first or last element of an array
* @type Model~modifierFunction
*/ */
lastStepModifierFunctions.$pop = (obj, field, value) => { lastStepModifierFunctions.$pop = (obj, field, value) => {
if (!Array.isArray(obj[field])) throw new Error('Can\'t $pop an element from non-array values') 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 * Removes all instances of a value from an existing array
* @type Model~modifierFunction
*/ */
lastStepModifierFunctions.$pull = (obj, field, value) => { lastStepModifierFunctions.$pull = (obj, field, value) => {
if (!Array.isArray(obj[field])) throw new Error('Can\'t $pull an element from non-array values') 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 * Increment a numeric field's value
* @type Model~modifierFunction
*/ */
lastStepModifierFunctions.$inc = (obj, field, value) => { lastStepModifierFunctions.$inc = (obj, field, value) => {
if (typeof value !== 'number') throw new Error(`${value} must be a number`) 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 * 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) => { lastStepModifierFunctions.$max = (obj, field, value) => {
if (typeof obj[field] === 'undefined') 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 * 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) => { lastStepModifierFunctions.$min = (obj, field, value) => {
if (typeof obj[field] === 'undefined') obj[field] = value if (typeof obj[field] === 'undefined') obj[field] = value
else if (value < obj[field]) 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 createModifierFunction = modifier => (obj, field, value) => {
const fieldParts = typeof field === 'string' ? field.split('.') : field 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 * Modify a DB object according to an update query
* @param {document} obj
* @param {query} updateQuery
* @return {document}
*/ */
const modify = (obj, updateQuery) => { const modify = (obj, updateQuery) => {
const keys = Object.keys(updateQuery) const keys = Object.keys(updateQuery)
@ -441,8 +487,9 @@ const modify = (obj, updateQuery) => {
/** /**
* Get a value from object with dot notation * Get a value from object with dot notation
* @param {Object} obj * @param {object} obj
* @param {String} field * @param {string} field
* @return {*}
*/ */
const getDotValue = (obj, field) => { const getDotValue = (obj, field) => {
const fieldParts = typeof field === 'string' ? field.split('.') : 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 * Things are defined as any native types (string, number, boolean, null, date) and objects
* In the case of object, we check deep equality * In the case of object, we check deep equality
* Returns true if they are, false otherwise * Returns true if they are, false otherwise
* @param {*} a
* @param {*} a
* @return {boolean}
*/ */
const areThingsEqual = (a, b) => { const areThingsEqual = (a, b) => {
// Strings, booleans, numbers, null // Strings, booleans, numbers, null
@ -513,6 +563,9 @@ const areThingsEqual = (a, b) => {
/** /**
* Check that two values are comparable * Check that two values are comparable
* @param {*} a
* @param {*} a
* @return {boolean}
*/ */
const areComparable = (a, b) => { const areComparable = (a, b) => {
if ( if (
@ -530,20 +583,47 @@ const areComparable = (a, b) => {
} }
/** /**
* @callback Model~comparisonOperator
* Arithmetic and comparison operators * Arithmetic and comparison operators
* @param {Native value} a Value in the object * @param {*} a Value in the object
* @param {Native value} b Value in the query * @param {*} b Value in the query
* @return {boolean}
*/
/**
* Lower than
* @type Model~comparisonOperator
*/ */
comparisonFunctions.$lt = (a, b) => areComparable(a, b) && a < b 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 comparisonFunctions.$lte = (a, b) => areComparable(a, b) && a <= b
/**
* Greater than
* @type Model~comparisonOperator
*/
comparisonFunctions.$gt = (a, b) => areComparable(a, b) && a > b 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 comparisonFunctions.$gte = (a, b) => areComparable(a, b) && a >= b
/**
* Does not equal
* @type Model~comparisonOperator
*/
comparisonFunctions.$ne = (a, b) => a === undefined || !areThingsEqual(a, b) comparisonFunctions.$ne = (a, b) => a === undefined || !areThingsEqual(a, b)
/**
* Is in Array
* @type Model~comparisonOperator
*/
comparisonFunctions.$in = (a, b) => { 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')
@ -553,13 +633,20 @@ comparisonFunctions.$in = (a, b) => {
return false return false
} }
/**
* Is not in Array
* @type Model~comparisonOperator
*/
comparisonFunctions.$nin = (a, b) => { 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
* @type Model~comparisonOperator
*/
comparisonFunctions.$regex = (a, b) => { 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')
@ -567,27 +654,38 @@ comparisonFunctions.$regex = (a, b) => {
else return b.test(a) 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 // 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 (exists || exists === '') exists = true if (b || b === '') b = true
else exists = false else b = false
if (value === undefined) return !exists if (a === undefined) return !b
else return exists else return b
} }
// Specific to arrays /**
comparisonFunctions.$size = (obj, value) => { * Specific to Arrays, returns true if a length equals b
if (!Array.isArray(obj)) return false * @type Model~comparisonOperator
if (value % 1 !== 0) throw new Error('$size operator called without an integer') */
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 * Specific to Arrays, returns true if some elements of a match the query b
return obj.some(el => match(el, value)) * @type Model~comparisonOperator
*/
comparisonFunctions.$elemMatch = (a, b) => {
if (!Array.isArray(a)) return false
return a.some(el => match(el, b))
} }
arrayComparisonFunctions.$size = true arrayComparisonFunctions.$size = true
@ -595,8 +693,9 @@ arrayComparisonFunctions.$elemMatch = true
/** /**
* Match any of the subqueries * Match any of the subqueries
* @param {Model} obj * @param {document} obj
* @param {Array of Queries} query * @param {query[]} query
* @return {boolean}
*/ */
logicalOperators.$or = (obj, query) => { logicalOperators.$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')
@ -610,8 +709,9 @@ logicalOperators.$or = (obj, query) => {
/** /**
* Match all of the subqueries * Match all of the subqueries
* @param {Model} obj * @param {document} obj
* @param {Array of Queries} query * @param {query[]} query
* @return {boolean}
*/ */
logicalOperators.$and = (obj, query) => { 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')
@ -625,15 +725,23 @@ logicalOperators.$and = (obj, query) => {
/** /**
* Inverted match of the query * Inverted match of the query
* @param {Model} obj * @param {document} obj
* @param {Query} query * @param {query} query
* @return {boolean}
*/ */
logicalOperators.$not = (obj, query) => !match(obj, query) logicalOperators.$not = (obj, query) => !match(obj, query)
/**
* @callback Model~whereCallback
* @param {document} obj
* @return {boolean}
*/
/** /**
* Use a function to match * Use a function to match
* @param {Model} obj * @param {document} obj
* @param {Query} query * @param {Model~whereCallback} fn
* @return {boolean}
*/ */
logicalOperators.$where = (obj, fn) => { logicalOperators.$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')
@ -646,8 +754,9 @@ logicalOperators.$where = (obj, fn) => {
/** /**
* Tell if a given document matches a query * Tell if a given document matches a query
* @param {Object} obj Document to check * @param {document} obj Document to check
* @param {Object} query * @param {query} query
* @return {boolean}
*/ */
const match = (obj, query) => { const match = (obj, query) => {
// Primitive query against a primitive type // 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 * 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
*/ */
/**
* 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) { function matchQueryPart (obj, queryKey, queryValue, treatObjAsValue) {
const objValue = getDotValue(obj, queryKey) const objValue = getDotValue(obj, queryKey)

@ -63,7 +63,7 @@ class Persistence {
/** /**
* @callback Persistence~persistCachedDatabaseCallback * @callback Persistence~persistCachedDatabaseCallback
* @param {Error?} err * @param {?Error} err
*/ */
/** /**
@ -110,7 +110,7 @@ class Persistence {
/** /**
* @callback Persistence~compactDataFileCallback * @callback Persistence~compactDataFileCallback
* @param {Error?} err * @param {?Error} err
*/ */
/** /**
@ -153,7 +153,7 @@ class Persistence {
/** /**
* @callback Persistence~persistNewStateCallback * @callback Persistence~persistNewStateCallback
* @param {Error?} err * @param {?Error} err
*/ */
/** /**
@ -233,8 +233,8 @@ class Persistence {
/** /**
* @callback Persistence~treatRawStreamCallback * @callback Persistence~treatRawStreamCallback
* @param {Error?} err * @param {?Error} err
* @param {{data: document[], indexes: Object.<string, rawIndex>}} data * @param {{data: document[], indexes: Object.<string, rawIndex>}?} data
*/ */
/** /**
@ -297,7 +297,7 @@ class Persistence {
/** /**
* @callback Persistence~loadDatabaseCallback * @callback Persistence~loadDatabaseCallback
* @param {Error?} err * @param {?Error} err
*/ */
/** /**
@ -361,7 +361,7 @@ class Persistence {
/** /**
* @callback Persistence~ensureDirectoryExistsCallback * @callback Persistence~ensureDirectoryExistsCallback
* @param {Error?} err * @param {?Error} err
*/ */
/** /**

Loading…
Cancel
Save