@ -790,7 +790,7 @@ var model = require('./model')
* Create a new cursor for this collection
* @ param { Datastore } db - The datastore this cursor is bound to
* @ param { Query } query - The query this cursor will operate on
* @ param { Function } execD n - Handler to be executed after cursor has found the results and before the callback passed to find / findOne / update / remove
* @ param { Function } execF n - Handler to be executed after cursor has found the results and before the callback passed to find / findOne / update / remove
* /
function Cursor ( db , query , execFn ) {
this . db = db ;
@ -862,7 +862,19 @@ Cursor.prototype.project = function (candidates) {
// Do the actual projection
candidates . forEach ( function ( candidate ) {
var toPush = action === 1 ? _ . pick ( candidate , keys ) : _ . omit ( candidate , keys ) ;
var toPush ;
if ( action === 1 ) { // pick-type projection
toPush = { $set : { } } ;
keys . forEach ( function ( k ) {
toPush . $set [ k ] = model . getDotValue ( candidate , k ) ;
if ( toPush . $set [ k ] === undefined ) { delete toPush . $set [ k ] ; }
} ) ;
toPush = model . modify ( { } , toPush ) ;
} else { // omit-type projection
toPush = { $unset : { } } ;
keys . forEach ( function ( k ) { toPush . $unset [ k ] = true } ) ;
toPush = model . modify ( candidate , toPush ) ;
}
if ( keepId ) {
toPush . _id = candidate . _id ;
} else {
@ -882,24 +894,34 @@ Cursor.prototype.project = function (candidates) {
*
* @ param { Function } callback - Signature : err , results
* /
Cursor . prototype . _exec = function ( callback ) {
var candidates = this . db . getCandidates ( this . query )
, res = [ ] , added = 0 , skipped = 0 , self = this
Cursor . prototype . _exec = function ( _callback ) {
var res = [ ] , added = 0 , skipped = 0 , self = this
, error = null
, i , keys , key
;
function callback ( error , res ) {
if ( self . execFn ) {
return self . execFn ( error , res , _callback ) ;
} else {
return _callback ( error , res ) ;
}
}
this . db . getCandidates ( this . query , function ( err , candidates ) {
if ( err ) { return callback ( err ) ; }
try {
for ( i = 0 ; i < candidates . length ; i += 1 ) {
if ( model . match ( candidates [ i ] , this . query ) ) {
if ( model . match ( candidates [ i ] , self . query ) ) {
// If a sort is defined, wait for the results to be sorted before applying limit and skip
if ( ! this . _sort ) {
if ( this . _skip && this . _skip > skipped ) {
if ( ! self . _sort ) {
if ( self . _skip && self . _skip > skipped ) {
skipped += 1 ;
} else {
res . push ( candidates [ i ] ) ;
added += 1 ;
if ( this . _limit && this . _limit <= added ) { break ; }
if ( self . _limit && self . _limit <= added ) { break ; }
}
} else {
res . push ( candidates [ i ] ) ;
@ -911,8 +933,8 @@ Cursor.prototype._exec = function(callback) {
}
// Apply all sorts
if ( this . _sort ) {
keys = Object . keys ( this . _sort ) ;
if ( self . _sort ) {
keys = Object . keys ( self . _sort ) ;
// Sorting
var criteria = [ ] ;
@ -933,25 +955,22 @@ Cursor.prototype._exec = function(callback) {
} ) ;
// Applying limit and skip
var limit = this . _limit || res . length
, skip = this . _skip || 0 ;
var limit = self . _limit || res . length
, skip = self . _skip || 0 ;
res = res . slice ( skip , skip + limit ) ;
}
// Apply projection
try {
res = this . project ( res ) ;
res = self . project ( res ) ;
} catch ( e ) {
error = e ;
res = undefined ;
}
if ( this . execFn ) {
return this . execFn ( error , res , callback ) ;
} else {
return callback ( error , res ) ;
}
} ) ;
} ;
Cursor . prototype . exec = function ( ) {
@ -1115,6 +1134,7 @@ function Datastore (options) {
// binary is always well-balanced
this . indexes = { } ;
this . indexes . _id = new Index ( { fieldName : '_id' , unique : true } ) ;
this . ttlIndexes = { } ;
// Queue a load of the database right away and call the onload handler
// By default (no onload handler), if there is an error there, no operation will be possible so warn the user by throwing an exception
@ -1161,6 +1181,7 @@ Datastore.prototype.resetIndexes = function (newData) {
* @ param { String } options . fieldName
* @ param { Boolean } options . unique
* @ param { Boolean } options . sparse
* @ param { Number } options . expireAfterSeconds - Optional , if set this index becomes a TTL index ( only works on Date fields , not arrays of Date )
* @ param { Function } cb Optional callback , signature : err
* /
Datastore . prototype . ensureIndex = function ( options , cb ) {
@ -1177,6 +1198,7 @@ Datastore.prototype.ensureIndex = function (options, cb) {
if ( this . indexes [ options . fieldName ] ) { return callback ( null ) ; }
this . indexes [ options . fieldName ] = new Index ( options ) ;
if ( options . expireAfterSeconds !== undefined ) { this . ttlIndexes [ options . fieldName ] = options . expireAfterSeconds ; } // With this implementation index creation is not necessary to ensure TTL but we stick with MongoDB's API here
try {
this . indexes [ options . fieldName ] . insert ( this . getAllData ( ) ) ;
@ -1289,12 +1311,25 @@ Datastore.prototype.updateIndexes = function (oldDoc, newDoc) {
* One way to make it better would be to enable the use of multiple indexes if the first usable index
* returns too much data . I may do it in the future .
*
* TODO : needs to be moved to the Cursor module
* Returned candidates will be scanned to find and remove all expired documents
*
* @ param { Query } query
* @ param { Boolean } dontExpireStaleDocs Optional , defaults to false , if true don 't remove stale docs. Useful for the remove function which shouldn' t be impacted by expirations
* @ param { Function } callback Signature err , docs
* /
Datastore . prototype . getCandidates = function ( query ) {
Datastore . prototype . getCandidates = function ( query , dontExpireStaleDocs , callback ) {
var indexNames = Object . keys ( this . indexes )
, self = this
, usableQueryKeys ;
if ( typeof dontExpireStaleDocs === 'function' ) {
callback = dontExpireStaleDocs ;
dontExpireStaleDocs = false ;
}
async . waterfall ( [
// STEP 1: get candidates list by checking indexes from most to least frequent usecase
function ( cb ) {
// For a basic match
usableQueryKeys = [ ] ;
Object . keys ( query ) . forEach ( function ( k ) {
@ -1304,7 +1339,7 @@ Datastore.prototype.getCandidates = function (query) {
} ) ;
usableQueryKeys = _ . intersection ( usableQueryKeys , indexNames ) ;
if ( usableQueryKeys . length > 0 ) {
return this . indexes [ usableQueryKeys [ 0 ] ] . getMatching ( query [ usableQueryKeys [ 0 ] ] ) ;
return cb ( null , self . indexes [ usableQueryKeys [ 0 ] ] . getMatching ( query [ usableQueryKeys [ 0 ] ] ) ) ;
}
// For a $in match
@ -1316,7 +1351,7 @@ Datastore.prototype.getCandidates = function (query) {
} ) ;
usableQueryKeys = _ . intersection ( usableQueryKeys , indexNames ) ;
if ( usableQueryKeys . length > 0 ) {
return this . indexes [ usableQueryKeys [ 0 ] ] . getMatching ( query [ usableQueryKeys [ 0 ] ] . $in ) ;
return cb ( null , self . indexes [ usableQueryKeys [ 0 ] ] . getMatching ( query [ usableQueryKeys [ 0 ] ] . $in ) ) ;
}
// For a comparison match
@ -1328,11 +1363,37 @@ Datastore.prototype.getCandidates = function (query) {
} ) ;
usableQueryKeys = _ . intersection ( usableQueryKeys , indexNames ) ;
if ( usableQueryKeys . length > 0 ) {
return this . indexes [ usableQueryKeys [ 0 ] ] . getBetweenBounds ( query [ usableQueryKeys [ 0 ] ] ) ;
return cb ( null , self . indexes [ usableQueryKeys [ 0 ] ] . getBetweenBounds ( query [ usableQueryKeys [ 0 ] ] ) ) ;
}
// By default, return all the DB data
return this . getAllData ( ) ;
return cb ( null , self . getAllData ( ) ) ;
}
// STEP 2: remove all expired documents
, function ( docs ) {
if ( dontExpireStaleDocs ) { return callback ( null , docs ) ; }
var expiredDocsIds = [ ] , validDocs = [ ] , ttlIndexesFieldNames = Object . keys ( self . ttlIndexes ) ;
docs . forEach ( function ( doc ) {
var valid = true ;
ttlIndexesFieldNames . forEach ( function ( i ) {
if ( doc [ i ] !== undefined && util . isDate ( doc [ i ] ) && Date . now ( ) > doc [ i ] . getTime ( ) + self . ttlIndexes [ i ] * 1000 ) {
valid = false ;
}
} ) ;
if ( valid ) { validDocs . push ( doc ) ; } else { expiredDocsIds . push ( doc . _id ) ; }
} ) ;
async . eachSeries ( expiredDocsIds , function ( _id , cb ) {
self . _remove ( { _id : _id } , { } , function ( err ) {
if ( err ) { return callback ( err ) ; }
return cb ( ) ;
} ) ;
} , function ( err ) {
return callback ( null , validDocs ) ;
} ) ;
} ] ) ;
} ;
@ -1542,7 +1603,20 @@ Datastore.prototype.findOne = function (query, projection, callback) {
* options . multi If true , can update multiple documents ( defaults to false )
* options . upsert If true , document is inserted if the query doesn ' t match anything
* options . returnUpdatedDocs Defaults to false , if true return as third argument the array of updated matched documents ( even if no change actually took place )
* @ param { Function } cb Optional callback , signature : err , numReplaced , upsert ( set to true if the update was in fact an upsert )
* @ param { Function } cb Optional callback , signature : ( err , numAffected , affectedDocuments , upsert )
* If update was an upsert , upsert flag is set to true
* affectedDocuments can be one of the following :
* * For an upsert , the upserted document
* * For an update with returnUpdatedDocs option false , null
* * For an update with returnUpdatedDocs true and multi false , the updated document
* * For an update with returnUpdatedDocs true and multi true , the array of updated documents
*
* WARNING : The API was changed between v1 . 7.4 and v1 . 8 , for consistency and readability reasons . Prior and including to v1 . 7.4 ,
* the callback signature was ( err , numAffected , updated ) where updated was the updated document in case of an upsert
* or the array of updated documents for an update if the returnUpdatedDocs option was true . That meant that the type of
* affectedDocuments in a non multi update depended on whether there was an upsert or not , leaving only two ways for the
* user to check whether an upsert had occured : checking the type of affectedDocuments or running another find query on
* the whole dataset to check its size . Both options being ugly , the breaking change was necessary .
*
* @ api private Use Datastore . update which has the same signature
* /
@ -1588,16 +1662,16 @@ Datastore.prototype._update = function (query, updateQuery, options, cb) {
return self . _insert ( toBeInserted , function ( err , newDoc ) {
if ( err ) { return callback ( err ) ; }
return callback ( null , 1 , newDoc ) ;
return callback ( null , 1 , newDoc , true ) ;
} ) ;
}
} ) ;
}
, function ( ) { // Perform the update
var modifiedDoc
, candidates = self . getCandidates ( query )
, modifications = [ ]
;
var modifiedDoc , modifications = [ ] , createdAt ;
self . getCandidates ( query , function ( err , candidates ) {
if ( err ) { return callback ( err ) ; }
// Preparing update (if an error is thrown here neither the datafile nor
// the in-memory indexes are affected)
@ -1605,8 +1679,12 @@ Datastore.prototype._update = function (query, updateQuery, options, cb) {
for ( i = 0 ; i < candidates . length ; i += 1 ) {
if ( model . match ( candidates [ i ] , query ) && ( multi || numReplaced === 0 ) ) {
numReplaced += 1 ;
if ( self . timestampData ) { createdAt = candidates [ i ] . createdAt ; }
modifiedDoc = model . modify ( candidates [ i ] , updateQuery ) ;
if ( self . timestampData ) { modifiedDoc . updatedAt = new Date ( ) ; }
if ( self . timestampData ) {
modifiedDoc . createdAt = createdAt ;
modifiedDoc . updatedAt = new Date ( ) ;
}
modifications . push ( { oldDoc : candidates [ i ] , newDoc : modifiedDoc } ) ;
}
}
@ -1630,12 +1708,14 @@ Datastore.prototype._update = function (query, updateQuery, options, cb) {
} else {
var updatedDocsDC = [ ] ;
updatedDocs . forEach ( function ( doc ) { updatedDocsDC . push ( model . deepCopy ( doc ) ) ; } ) ;
if ( ! multi ) { updatedDocsDC = updatedDocsDC [ 0 ] ; }
return callback ( null , numReplaced , updatedDocsDC ) ;
}
} ) ;
}
] ) ;
} ) ;
} ] ) ;
} ;
Datastore . prototype . update = function ( ) {
this . executor . push ( { this : this , fn : this . _update , arguments : arguments } ) ;
} ;
@ -1653,17 +1733,16 @@ Datastore.prototype.update = function () {
* /
Datastore . prototype . _remove = function ( query , options , cb ) {
var callback
, self = this
, numRemoved = 0
, multi
, removedDocs = [ ]
, candidates = this . getCandidates ( query )
, self = this , numRemoved = 0 , removedDocs = [ ] , multi
;
if ( typeof options === 'function' ) { cb = options ; options = { } ; }
callback = cb || function ( ) { } ;
multi = options . multi !== undefined ? options . multi : false ;
this . getCandidates ( query , true , function ( err , candidates ) {
if ( err ) { return callback ( err ) ; }
try {
candidates . forEach ( function ( d ) {
if ( model . match ( d , query ) && ( multi || numRemoved === 0 ) ) {
@ -1678,16 +1757,15 @@ Datastore.prototype._remove = function (query, options, cb) {
if ( err ) { return callback ( err ) ; }
return callback ( null , numRemoved ) ;
} ) ;
} ) ;
} ;
Datastore . prototype . remove = function ( ) {
this . executor . push ( { this : this , fn : this . _remove , arguments : arguments } ) ;
} ;
module . exports = Datastore ;
} , { "./cursor" : 5 , "./customUtils" : 6 , "./executor" : 8 , "./indexes" : 9 , "./model" : 10 , "./persistence" : 11 , "async" : 13 , "events" : 1 , "underscore" : 19 , "util" : 3 } ] , 8 : [ function ( require , module , exports ) {
@ -1704,17 +1782,16 @@ function Executor () {
// This queue will execute all commands, one-by-one in order
this . queue = async . queue ( function ( task , cb ) {
var callback
, lastArg = task . arguments [ task . arguments . length - 1 ]
, i , newArguments = [ ]
;
var newArguments = [ ] ;
// task.arguments is an array-like object on which adding a new field doesn't work, so we transform it into a real array
for ( i = 0 ; i < task . arguments . length ; i += 1 ) { newArguments . push ( task . arguments [ i ] ) ; }
for ( var i = 0 ; i < task . arguments . length ; i += 1 ) { newArguments . push ( task . arguments [ i ] ) ; }
var lastArg = task . arguments [ task . arguments . length - 1 ] ;
// Always tell the queue task is complete. Execute callback if any was given.
if ( typeof lastArg === 'function' ) {
callback = function ( ) {
// Callback was supplied
newArguments [ newArguments . length - 1 ] = function ( ) {
if ( typeof setImmediate === 'function' ) {
setImmediate ( cb ) ;
} else {
@ -1722,11 +1799,12 @@ function Executor () {
}
lastArg . apply ( null , arguments ) ;
} ;
newArguments [ newArguments . length - 1 ] = callback ;
} else if ( ! lastArg && task . arguments . length !== 0 ) {
// false/undefined/null supplied as callbback
newArguments [ newArguments . length - 1 ] = function ( ) { cb ( ) ; } ;
} else {
callback = function ( ) { cb ( ) ; } ;
newArguments . push ( callback ) ;
// Nothing supplied as callback
newArguments . push ( function ( ) { cb ( ) ; } ) ;
}
@ -1741,7 +1819,8 @@ function Executor () {
* @ param { Object } task
* task . this - Object to use as this
* task . fn - Function to execute
* task . arguments - Array of arguments
* task . arguments - Array of arguments , IMPORTANT : only the last argument may be a function ( the callback )
* and the last argument cannot be false / undefined / null
* @ param { Boolean } forceQueuing Optional ( defaults to false ) force executor to queue task even if it is not ready
* /
Executor . prototype . push = function ( task , forceQueuing ) {
@ -2332,6 +2411,9 @@ lastStepModifierFunctions.$unset = function (obj, field, value) {
/ * *
* Push an element to the end of an array field
* 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
* /
lastStepModifierFunctions . $push = function ( obj , field , value ) {
// Create the array if it doesn't exist
@ -2339,13 +2421,33 @@ lastStepModifierFunctions.$push = function (obj, field, value) {
if ( ! util . isArray ( obj [ field ] ) ) { throw new Error ( "Can't $push an element on non-array values" ) ; }
if ( value !== null && typeof value === 'object' && value . $slice && value . $each === undefined ) {
value . $each = [ ] ;
}
if ( value !== null && typeof value === 'object' && value . $each ) {
if ( Object . keys ( value ) . length > 1 ) { throw new Error ( "Can't use another field in conjunction with $each" ) ; }
if ( Object . keys ( value ) . length >= 3 || ( Object . keys ( value ) . length === 2 && value . $slice === undefined ) ) { throw new Error ( "Can only use $slice in cunjunction with $each when $push to array " ) ; }
if ( ! util . isArray ( value . $each ) ) { throw new Error ( "$each requires an array value" ) ; }
value . $each . forEach ( function ( v ) {
obj [ field ] . push ( v ) ;
} ) ;
if ( value . $slice === undefined || typeof value . $slice !== 'number' ) { return ; }
if ( value . $slice === 0 ) {
obj [ field ] = [ ] ;
} else {
var start , end , n = obj [ field ] . length ;
if ( value . $slice < 0 ) {
start = Math . max ( 0 , n + value . $slice ) ;
end = n ;
} else if ( value . $slice > 0 ) {
start = 0 ;
end = Math . min ( n , value . $slice ) ;
}
obj [ field ] = obj [ field ] . slice ( start , end ) ;
}
} else {
obj [ field ] . push ( value ) ;
}
@ -2431,6 +2533,28 @@ lastStepModifierFunctions.$inc = function (obj, field, value) {
}
} ;
/ * *
* Updates the value of the field , only if specified field is greater than the current value of the field
* /
lastStepModifierFunctions . $max = function ( obj , field , value ) {
if ( typeof obj [ field ] === 'undefined' ) {
obj [ field ] = value ;
} else if ( value > obj [ field ] ) {
obj [ field ] = value ;
}
} ;
/ * *
* Updates the value of the field , only if specified field is smaller than the current value of the field
* /
lastStepModifierFunctions . $min = function ( obj , field , value ) {
if ( typeof obj [ field ] === 'undefined' ) {
obj [ field ] = value ;
} else if ( value < obj [ field ] ) {
obj [ field ] = value ;
}
} ;
// Given its name, create the complete modifier function
function createModifierFunction ( modifier ) {
return function ( obj , field , value ) {
@ -2439,7 +2563,10 @@ function createModifierFunction (modifier) {
if ( fieldParts . length === 1 ) {
lastStepModifierFunctions [ modifier ] ( obj , field , value ) ;
} else {
obj [ fieldParts [ 0 ] ] = obj [ fieldParts [ 0 ] ] || { } ;
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 ) ;
}
} ;
@ -2480,7 +2607,7 @@ function modify (obj, updateQuery) {
if ( ! modifierFunctions [ m ] ) { throw new Error ( "Unknown modifier " + m ) ; }
// Can't rely on Object.keys throwing on non objects since ES6{
// Can't rely on Object.keys throwing on non objects since ES6
// Not 100% satisfying as non objects can be interpreted as objects but no false negatives so we can live with it
if ( typeof updateQuery [ m ] !== 'object' ) {
throw new Error ( "Modifier " + m + "'s argument must be an object" ) ;
@ -2667,7 +2794,20 @@ comparisonFunctions.$size = function (obj, value) {
return ( obj . length == value ) ;
} ;
comparisonFunctions . $elemMatch = function ( obj , value ) {
if ( ! util . isArray ( obj ) ) { return false ; }
var i = obj . length ;
var result = false ; // Initialize result
while ( i -- ) {
if ( match ( obj [ i ] , value ) ) { // If match for array element, return true
result = true ;
break ;
}
}
return result ;
} ;
arrayComparisonFunctions . $size = true ;
arrayComparisonFunctions . $elemMatch = true ;
/ * *