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

Timothée Rebours 3 years ago
parent 7ca620df33
commit 9dc0a22509
  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 || {}
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
}
/**

@ -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 }))
})

@ -5,7 +5,13 @@
* Querying, update
*/
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 = {}
@ -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)

@ -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.<string, rawIndex>}} data
* @param {?Error} err
* @param {{data: document[], indexes: Object.<string, rawIndex>}?} 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
*/
/**

Loading…
Cancel
Save