Built browser version

pull/2/head
Louis Chatriot 9 years ago
parent 1695426cd9
commit 08f37b9cc2
  1. 250
      browser-version/out/nedb.js
  2. 8
      browser-version/out/nedb.min.js

@ -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} execDn - Handler to be executed after cursor has found the results and before the callback passed to find/findOne/update/remove
* @param {Function} execFn - 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;
/**

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save