nestdb
Version:
The JavaScript Database for Node.js, nwjs, Electron, and the browser
965 lines (829 loc) • 30.5 kB
JavaScript
// Node.js core modules
var util = require('util')
, EventEmitter = require('events')
// Userland modules
, async = require('async')
, _ = require('underscore')
// Local modules
, customUtils = require('./customUtils')
, model = require('./model')
, Executor = require('./executor')
, Index = require('./indexes')
, Persistence = require('./persistence')
, Cursor = require('./cursor')
;
/**
* Create a new collection
*
* @param {String} options.filename Optional, datastore will be in-memory only if not provided
* @param {Boolean} options.timestampData Optional, defaults to false. If set to true, createdAt and updatedAt will be created and populated automatically (if not specified by user)
* @param {Boolean} options.inMemoryOnly Optional, defaults to false
* @param {Boolean} options.autoload Optional, defaults to false
* @param {Function} options.onload Optional, if autoload is used this will be called after the load database with the error object as parameter. If you don't pass it the error will be thrown
* @param {Function} options.idGenerator Optional, if set, this function will be used for generating IDs. It takes no arguments and should return a unique string.
* @param {Function} options.afterSerialization Optional, serialization hook. Must be symmetrical to `options.beforeDeserialization`.
* @param {Function} options.beforeDeserialization Optional, deserialization hook. Must be symmetrical to options.afterSerialization`.
* @param {Number} options.corruptAlertThreshold Optional, threshold after which an alert is thrown if too much data is corrupt
* @param {Function} options.compareStrings Optional, string comparison function that overrides default for sorting
* @param {Object} options.storage Optional, custom storage engine for the database files. Must implement all methods exported by the standard "storage" module included in NestDB
*
* @fires Datastore.created
* @fires Datastore#loaded
* @fires Datastore#compacted
* @fires Datastore#inserted
* @fires Datastore#updated
* @fires Datastore#removed
* @fires Datastore#destroyed
* @fires Datastore.destroyed
*
* Event Emitter
* - Static Events
* - "created": Emitted whenever a Datastore is fully loaded OR newly created
* - callback: function(db) { ... }
* - context: Datastore
* - "destroyed": Emitted whenever a Datastore is fully destroyed
* - callback: function(db) { ... }
* - context: Datastore
*
* - Instance Events
* - "loaded": Emitted when this Datastore is fully loaded
* - callback: function() { ... }
* - context: this (a.k.a. `db`)
* - "compacted": Emitted whenever a compaction operation is completed for this Datastore
* - callback: function() { ... }
* - context: this (a.k.a. `db`)
* - "inserted": Emitted whenever a new document is inserted for this Datastore
* - callback: function(newDoc) { ... }
* - context: this (a.k.a. `db`)
* - "updated": Emitted whenever an existing document is updated for this Datastore
* - callback: function(newDoc, oldDoc) { ... }
* - context: this (a.k.a. `db`)
* - "removed": Emitted whenever an existing document is removed for this Datastore
* - callback: function(oldDoc) { ... }
* - context: this (a.k.a. `db`)
* - "destroyed": Emitted when this Datastore is fully destroyed
* - callback: function() { ... }
* - context: this (a.k.a. `db`)
*/
function Datastore(options) {
if (!(this instanceof Datastore)) {
return new Datastore(options);
}
EventEmitter.call(this);
options = options || {};
var filename = options.filename;
this.inMemoryOnly = options.inMemoryOnly || false;
this.autoload = options.autoload || false;
this.timestampData = options.timestampData || false;
if (typeof options.idGenerator === 'function') {
this._idGenerator = options.idGenerator;
}
// Determine whether in memory or persistent
if (!filename || typeof filename !== 'string' || filename.length === 0) {
this.filename = null;
this.inMemoryOnly = true;
} else {
this.filename = filename;
}
// String comparison function
this.compareStrings = options.compareStrings;
// Persistence handling
this.persistence = new Persistence({ db: this
, afterSerialization: options.afterSerialization
, beforeDeserialization: options.beforeDeserialization
, corruptAlertThreshold: options.corruptAlertThreshold
, storage: options.storage
});
// This new executor is ready if we don't use persistence
// If we do, it will only be ready once `load` is called
this.executor = new Executor();
if (this.inMemoryOnly) { this.executor.ready = true; }
// Indexed by field name, dot notation can be used
// _id is always indexed and since _ids are generated randomly the underlying
// 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
if (this.autoload) {
this.load(options.onload);
}
}
util.inherits(Datastore, EventEmitter);
//
// Also forcibly alter the Datastore (NeDB) object itself to be a static EventEmitter
//
EventEmitter.call(Datastore);
Object.keys(EventEmitter.prototype)
.forEach(function (key) {
if (typeof EventEmitter.prototype[key] === 'function') {
Datastore[key] = EventEmitter.prototype[key].bind(Datastore);
}
});
/**
* Load the database from the datafile, and trigger the execution of buffered commands if any
*
* @fires Datastore.created
* @fires Datastore#loaded
* @fires Datastore#compacted
*/
Datastore.prototype.load = function (cb) {
var self = this
, callback = cb || function (err) { if (err) { throw err; } }
, eventedCallback = function (err) {
async.setImmediate(function () {
if (!err) {
// Ensure there are listeners registered before making any unnecessary function calls to `emit`
if (self.constructor.listeners('created').length > 0) {
self.constructor.emit('created', self);
}
if (self.listeners('loaded').length > 0) {
self.emit('loaded');
}
}
callback(err);
});
};
this.executor.push({ this: this.persistence, fn: this.persistence.loadDatabase, arguments: [eventedCallback] }, true);
};
/**
* Backward compatibility with NeDB
* @deprecated
*/
Datastore.prototype.loadDatabase = Datastore.prototype.load;
/**
* Get an array of all the data in the database
*/
Datastore.prototype.getAllData = function () {
return this.indexes._id.getAll();
};
/**
* Reset all currently defined indexes
*/
Datastore.prototype.resetIndexes = function (newData) {
var self = this;
Object.keys(this.indexes).forEach(function (i) {
self.indexes[i].reset(newData);
});
};
/**
* Ensure an index is kept for this field. Same parameters as lib/indexes
* For now this function is synchronous, we need to test how much time it takes
* We use an async API for consistency with the rest of the code
* @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) {
var err
, callback = cb || function () {};
options = options || {};
if (!options.fieldName) {
err = new Error("Cannot create an index without a fieldName");
err.missingFieldName = true;
return callback(err);
}
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());
} catch (e) {
delete this.indexes[options.fieldName];
return callback(e);
}
// We may want to force all options to be persisted including defaults, not just the ones passed the index creation function
this.persistence.persistNewState([{ $$indexCreated: options }], function (err) {
if (err) { return callback(err); }
return callback(null);
});
};
/**
* Remove an index
* @param {String} fieldName
* @param {Function} cb Optional callback, signature: err
*/
Datastore.prototype.removeIndex = function (fieldName, cb) {
var callback = cb || function () {};
delete this.indexes[fieldName];
this.persistence.persistNewState([{ $$indexRemoved: fieldName }], function (err) {
if (err) { return callback(err); }
return callback(null);
});
};
/**
* Add one or several document(s) to all indexes
*/
Datastore.prototype.addToIndexes = function (doc) {
var i, failingIndex, error
, keys = Object.keys(this.indexes)
;
for (i = 0; i < keys.length; i += 1) {
try {
this.indexes[keys[i]].insert(doc);
} catch (e) {
failingIndex = i;
error = e;
break;
}
}
// If an error happened, we need to rollback the insert on all other indexes
if (error) {
for (i = 0; i < failingIndex; i += 1) {
this.indexes[keys[i]].remove(doc);
}
throw error;
}
};
/**
* Remove one or several document(s) from all indexes
*/
Datastore.prototype.removeFromIndexes = function (doc) {
var self = this;
Object.keys(this.indexes).forEach(function (i) {
self.indexes[i].remove(doc);
});
};
/**
* Update one or several documents in all indexes
* To update multiple documents, oldDoc must be an array of { oldDoc, newDoc } pairs
* If one update violates a constraint, all changes are rolled back
*/
Datastore.prototype.updateIndexes = function (oldDoc, newDoc) {
var i, failingIndex, error
, keys = Object.keys(this.indexes)
;
for (i = 0; i < keys.length; i += 1) {
try {
this.indexes[keys[i]].update(oldDoc, newDoc);
} catch (e) {
failingIndex = i;
error = e;
break;
}
}
// If an error happened, we need to rollback the update on all other indexes
if (error) {
for (i = 0; i < failingIndex; i += 1) {
this.indexes[keys[i]].revertUpdate(oldDoc, newDoc);
}
throw error;
}
};
/**
* Return the list of candidates for a given query
* Crude implementation for now, we return the candidates given by the first usable index if any
* We try the following query types, in this order: basic match, $in match, comparison match
* 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.
*
* 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, candidates
*/
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) {
if (typeof query[k] === 'string' || typeof query[k] === 'number' || typeof query[k] === 'boolean' || _.isDate(query[k]) || query[k] === null) {
usableQueryKeys.push(k);
}
});
usableQueryKeys = _.intersection(usableQueryKeys, indexNames);
if (usableQueryKeys.length > 0) {
return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]]));
}
// For a $in match
usableQueryKeys = [];
Object.keys(query).forEach(function (k) {
if (query[k] && query[k].hasOwnProperty('$in')) {
usableQueryKeys.push(k);
}
});
usableQueryKeys = _.intersection(usableQueryKeys, indexNames);
if (usableQueryKeys.length > 0) {
return cb(null, self.indexes[usableQueryKeys[0]].getMatching(query[usableQueryKeys[0]].$in));
}
// For a comparison match
usableQueryKeys = [];
Object.keys(query).forEach(function (k) {
if (query[k] && (query[k].hasOwnProperty('$lt') || query[k].hasOwnProperty('$lte') || query[k].hasOwnProperty('$gt') || query[k].hasOwnProperty('$gte'))) {
usableQueryKeys.push(k);
}
});
usableQueryKeys = _.intersection(usableQueryKeys, indexNames);
if (usableQueryKeys.length > 0) {
return cb(null, self.indexes[usableQueryKeys[0]].getBetweenBounds(query[usableQueryKeys[0]]));
}
// By default, return all the DB data
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 && _.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);
});
}]);
};
/**
* Insert a new document
*
* @private
* @see Datastore#insert
*/
Datastore.prototype._insert = function (newDoc, cb) {
var preparedDoc
, self = this
, origCallback = cb || function () {}
, callback = function (err, newDocs) {
var context = this
, args = arguments
;
async.setImmediate(function () {
if (!err) {
var newDocsArr = Array.isArray(newDocs) ? newDocs : [newDocs];
// Ensure there are listeners registered before making a bunch of unnecessary function calls to `emit`
if (self.listeners('inserted').length > 0) {
newDocsArr.forEach(function (newDoc) {
self.emit('inserted', newDoc);
});
}
}
origCallback.apply(context, args);
});
}
;
try {
preparedDoc = this.prepareDocumentForInsertion(newDoc)
this._insertInCache(preparedDoc);
} catch (e) {
return callback(e);
}
this.persistence.persistNewState(Array.isArray(preparedDoc) ? preparedDoc : [preparedDoc], function (err) {
if (err) { return callback(err); }
return callback(null, model.deepCopy(preparedDoc));
});
};
/**
* Default implementation for generating a unique _id
* @protected
*/
Datastore.prototype._idGenerator = function () {
return customUtils.uid(16);
};
/**
* Create a new _id that's not already in use
*/
Datastore.prototype.createNewId = function () {
var tentativeId;
// Try as many times as needed to get an unused _id. As explained in customUtils, the probability of this ever happening is extremely small, so this is O(1)
do {
tentativeId = this._idGenerator();
}
while (this.indexes._id.getMatching(tentativeId).length > 0);
return tentativeId;
};
/**
* Prepare a document (or array of documents) to be inserted in a database
* Meaning adds _id and timestamps if necessary on a copy of newDoc to avoid any side effect on user input
* @private
*/
Datastore.prototype.prepareDocumentForInsertion = function (newDoc) {
var preparedDoc, self = this;
if (Array.isArray(newDoc)) {
preparedDoc = [];
newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)); });
} else {
preparedDoc = model.deepCopy(newDoc);
if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId(); }
var now = new Date();
if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = now; }
if (this.timestampData && preparedDoc.updatedAt === undefined) { preparedDoc.updatedAt = now; }
model.checkObject(preparedDoc);
}
return preparedDoc;
};
/**
* If newDoc is an array of documents, this will insert all documents in the cache
* @private
*/
Datastore.prototype._insertInCache = function (preparedDoc) {
if (Array.isArray(preparedDoc)) {
this._insertMultipleDocsInCache(preparedDoc);
} else {
this.addToIndexes(preparedDoc);
}
};
/**
* If one insertion fails (e.g. because of a unique constraint), roll back all previous
* inserts and throws the error
* @private
*/
Datastore.prototype._insertMultipleDocsInCache = function (preparedDocs) {
var i, failingI, error;
for (i = 0; i < preparedDocs.length; i += 1) {
try {
this.addToIndexes(preparedDocs[i]);
} catch (e) {
error = e;
failingI = i;
break;
}
}
if (error) {
for (i = 0; i < failingI; i += 1) {
this.removeFromIndexes(preparedDocs[i]);
}
throw error;
}
};
/**
* Insert a new document
*
* @param {Object} newDoc New document to be inserted
* @param {Function} cb Optional callback, signature: err, insertedDoc
*
* @fires Datastore#inserted
*/
Datastore.prototype.insert = function () {
this.executor.push({ this: this, fn: this._insert, arguments: arguments });
};
/**
* Count all documents matching the query
* @param {Object} query MongoDB-style query
*/
Datastore.prototype.count = function(query, callback) {
var cursor = new Cursor(this, query, function(err, docs, callback) {
if (err) { return callback(err); }
return callback(null, docs.length);
});
if (typeof callback === 'function') {
cursor.exec(callback);
} else {
return cursor;
}
};
/**
* Find all documents matching the query
* If no callback is passed, we return the cursor so that user can limit, skip and finally exec
* @param {Object} query MongoDB-style query
* @param {Object} projection MongoDB-style projection
*/
Datastore.prototype.find = function (query, projection, callback) {
switch (arguments.length) {
case 1:
projection = {};
// callback is undefined, will return a cursor
break;
case 2:
if (typeof projection === 'function') {
callback = projection;
projection = {};
} // If not assume projection is an object and callback undefined
break;
}
var cursor = new Cursor(this, query, function(err, docs, callback) {
var res = [], i;
if (err) { return callback(err); }
for (i = 0; i < docs.length; i += 1) {
res.push(model.deepCopy(docs[i]));
}
return callback(null, res);
});
cursor.projection(projection);
if (typeof callback === 'function') {
cursor.exec(callback);
} else {
return cursor;
}
};
/**
* Find one document matching the query
* @param {Object} query MongoDB-style query
* @param {Object} projection MongoDB-style projection
*/
Datastore.prototype.findOne = function (query, projection, callback) {
switch (arguments.length) {
case 1:
projection = {};
// callback is undefined, will return a cursor
break;
case 2:
if (typeof projection === 'function') {
callback = projection;
projection = {};
} // If not assume projection is an object and callback undefined
break;
}
var cursor = new Cursor(this, query, function(err, docs, callback) {
if (err) { return callback(err); }
if (docs.length === 1) {
return callback(null, model.deepCopy(docs[0]));
} else {
return callback(null, null);
}
});
cursor.projection(projection).limit(1);
if (typeof callback === 'function') {
cursor.exec(callback);
} else {
return cursor;
}
};
/**
* Update all docs matching query
*
* @private
* @see Datastore#update
*/
Datastore.prototype._update = function (query, updateQuery, options, cb) {
var callback
, self = this
, numReplaced = 0
, multi
, upsert
, i
;
if (typeof options === 'function') { cb = options; options = {}; }
callback = cb || function () {};
multi = options.multi !== undefined ? options.multi : false;
upsert = options.upsert !== undefined ? options.upsert : false;
async.waterfall([
function (cb) { // If upsert option is set, check whether we need to insert the doc
if (!upsert) { return cb(); }
// Need to use an internal function not tied to the executor to avoid deadlock
var cursor = new Cursor(self, query);
cursor.limit(1)._exec(function (err, docs) {
if (err) { return callback(err); }
if (docs.length === 1) {
return cb();
} else {
var toBeInserted;
try {
model.checkObject(updateQuery);
// updateQuery is a simple object with no modifier, use it as the document to insert
toBeInserted = updateQuery;
} catch (e) {
// updateQuery contains modifiers, use the find query as the base,
// strip it from all operators and update it according to updateQuery
try {
toBeInserted = model.modify(model.deepCopy(query, true), updateQuery);
} catch (err) {
return callback(err);
}
}
return self._insert(toBeInserted, function (err, newDoc) {
if (err) { return callback(err); }
return callback(null, 1, newDoc, true);
});
}
});
}
, function () { // Perform the update
var modifiedDoc
, modifications = []
, createdAt
, eventedCallback = function(err /*, numReplaced, updatedDocs, upserted */) {
var context = this
, args = arguments
;
async.setImmediate(function () {
if (!err) {
if (modifications && modifications.length > 0) {
// Ensure there are listeners registered before making a bunch of unnecessary function calls to `emit`
if (self.listeners('updated').length > 0) {
modifications.forEach(function (mod) {
self.emit('updated', mod.newDoc, mod.oldDoc);
});
}
}
}
callback.apply(context, args);
});
}
;
self.getCandidates(query, function (err, candidates) {
if (err) { return eventedCallback(err); }
// Preparing update (if an error is thrown here neither the datafile nor
// the in-memory indexes are affected)
try {
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.createdAt = createdAt;
modifiedDoc.updatedAt = new Date();
}
modifications.push({ oldDoc: candidates[i], newDoc: modifiedDoc });
}
}
} catch (err) {
return eventedCallback(err);
}
// Change the docs in memory
try {
self.updateIndexes(modifications);
} catch (err) {
return eventedCallback(err);
}
// Update the datafile
var updatedDocs = _.pluck(modifications, 'newDoc');
self.persistence.persistNewState(updatedDocs, function (err) {
if (err) { return eventedCallback(err); }
if (!options.returnUpdatedDocs) {
return eventedCallback(null, numReplaced);
} else {
var updatedDocsDC = [];
updatedDocs.forEach(function (doc) { updatedDocsDC.push(model.deepCopy(doc)); });
if (!multi) { updatedDocsDC = updatedDocsDC[0]; }
return eventedCallback(null, numReplaced, updatedDocsDC);
}
});
});
}]);
};
/**
* Update all docs matching query
*
* @param {Object} query
* @param {Object} updateQuery
* @param {Object} options Optional options
* 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, 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
*
* @fires Datastore#updated
* @fires Datastore#inserted
*/
Datastore.prototype.update = function () {
this.executor.push({ this: this, fn: this._update, arguments: arguments });
};
/**
* Remove all docs matching the query
*
* @private
* @see Datastore#remove
*/
Datastore.prototype._remove = function (query, options, cb) {
var origCallback
, callback
, self = this
, numRemoved = 0
, removalDocs = []
, removedFullDocs = []
, multi
;
if (typeof options === 'function') { cb = options; options = {}; }
origCallback = cb || function () {};
multi = options.multi !== undefined ? options.multi : false;
callback = function(err /*, numRemoved */) {
var context = this
, args = arguments
;
async.setImmediate(function () {
if (!err) {
if (removedFullDocs && removedFullDocs.length > 0) {
// Ensure there are listeners registered before making a bunch of unnecessary function calls to `emit`
if (self.listeners('removed').length > 0) {
removedFullDocs.forEach(function (oldDoc) {
self.emit('removed', oldDoc);
});
}
}
}
origCallback.apply(context, args);
});
};
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)) {
numRemoved += 1;
removalDocs.push({ $$deleted: true, _id: d._id });
removedFullDocs.push(d);
self.removeFromIndexes(d);
}
});
} catch (err) { return callback(err); }
self.persistence.persistNewState(removalDocs, function (err) {
if (err) { return callback(err); }
return callback(null, numRemoved);
});
});
};
/**
* Remove all docs matching the query
*
* @param {Object} query
* @param {Object} options Optional options
* options.multi If true, can update multiple documents (defaults to false)
* @param {Function} cb Optional callback, signature: err, numRemoved
*
* @fires Datastore#removed
*/
Datastore.prototype.remove = function () {
this.executor.push({ this: this, fn: this._remove, arguments: arguments });
};
/**
* Remove all docs and then destroy the database's persistent datafile, if any
*
* @param {Function} cb Optional callback, signature: err
*
* @private
* @see Datastore#destroy
*/
Datastore.prototype._destroy = function (cb) {
var self = this
, callback = cb || function () {};
self._remove({}, { multi: true }, function (err /*, numRemoved */) {
if (err) {
return callback(err);
}
self.persistence.destroyDatabase(callback);
});
};
/**
* Remove all docs and then destroy the database's persistent datafile, if any
*
* @param {Function} cb Optional callback, signature: err
*
* @fires Datastore#destroyed
* @fires Datastore.destroyed
*/
Datastore.prototype.destroy = function (cb) {
var self = this
, callback = cb || function () {}
, eventedCallback = function (err) {
async.setImmediate(function () {
if (!err) {
// Ensure there are listeners registered before making any unnecessary function calls to `emit`
if (self.listeners('destroyed').length > 0) {
self.emit('destroyed');
}
if (self.constructor.listeners('destroyed').length > 0) {
self.constructor.emit('destroyed', self);
}
}
callback(err);
});
};
this.executor.push({ this: this, fn: this._destroy, arguments: [eventedCallback] });
};
/**
* Augment `Datastore` with user-defined extensions/patches
* @param {Function|Object} ext Must either be a function that takes one argument of `Datastore`, or a non-empty object
*/
Datastore.plugin = function (ext) {
if (typeof ext === 'function') { // function style for plugins
ext(Datastore);
} else if (typeof ext !== 'object' || !ext || Object.keys(ext).length === 0){
throw new Error('Invalid plugin: got \"' + ext + '\", expected an object or a function');
} else {
Object.keys(ext).forEach(function (id) { // object style for plugins
Datastore.prototype[id] = ext[id];
});
}
return Datastore;
};
module.exports = Datastore;