Fixed side effect bug

pull/2/head
Louis Chatriot 9 years ago
parent ef8905adf0
commit 7b3f3c2868
  1. 29
      lib/datastore.js
  2. 78
      test/db.test.js

@ -34,6 +34,7 @@ function Datastore (options) {
filename = options.filename; filename = options.filename;
this.inMemoryOnly = options.inMemoryOnly || false; this.inMemoryOnly = options.inMemoryOnly || false;
this.autoload = options.autoload || false; this.autoload = options.autoload || false;
this.timestampData = options.timestampData || false;
} }
// Determine whether in memory or persistent // Determine whether in memory or persistent
@ -61,7 +62,7 @@ function Datastore (options) {
// binary is always well-balanced // binary is always well-balanced
this.indexes = {}; this.indexes = {};
this.indexes._id = new Index({ fieldName: '_id', unique: true }); this.indexes._id = new Index({ fieldName: '_id', unique: true });
// Queue a load of the database right away and call the onload handler // 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 // 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.loadDatabase(options.onload || function (err) { if (this.autoload) { this.loadDatabase(options.onload || function (err) {
@ -283,17 +284,19 @@ Datastore.prototype.getCandidates = function (query) {
*/ */
Datastore.prototype._insert = function (newDoc, cb) { Datastore.prototype._insert = function (newDoc, cb) {
var callback = cb || function () {} var callback = cb || function () {}
, preparedDoc
; ;
try { try {
this._insertInCache(newDoc); preparedDoc = this.prepareDocumentForInsertion(newDoc)
this._insertInCache(preparedDoc);
} catch (e) { } catch (e) {
return callback(e); return callback(e);
} }
this.persistence.persistNewState(util.isArray(newDoc) ? newDoc : [newDoc], function (err) { this.persistence.persistNewState(util.isArray(preparedDoc) ? preparedDoc : [preparedDoc], function (err) {
if (err) { return callback(err); } if (err) { return callback(err); }
return callback(null, newDoc); return callback(null, model.deepCopy(preparedDoc));
}); });
}; };
@ -311,6 +314,7 @@ Datastore.prototype.createNewId = function () {
/** /**
* Prepare a document (or array of documents) to be inserted in a database * Prepare a document (or array of documents) to be inserted in a database
* Meaning adds _id and createdAt if necessary on a copy of newDoc to avoid any side effect on user input
* @api private * @api private
*/ */
Datastore.prototype.prepareDocumentForInsertion = function (newDoc) { Datastore.prototype.prepareDocumentForInsertion = function (newDoc) {
@ -320,9 +324,8 @@ Datastore.prototype.prepareDocumentForInsertion = function (newDoc) {
preparedDoc = []; preparedDoc = [];
newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)); }); newDoc.forEach(function (doc) { preparedDoc.push(self.prepareDocumentForInsertion(doc)); });
} else { } else {
if (newDoc._id === undefined) { newDoc._id = this.createNewId(); }
preparedDoc = model.deepCopy(newDoc); preparedDoc = model.deepCopy(newDoc);
//if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId(); } if (preparedDoc._id === undefined) { preparedDoc._id = this.createNewId(); }
if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = new Date(); } if (this.timestampData && preparedDoc.createdAt === undefined) { preparedDoc.createdAt = new Date(); }
model.checkObject(preparedDoc); model.checkObject(preparedDoc);
} }
@ -334,11 +337,11 @@ Datastore.prototype.prepareDocumentForInsertion = function (newDoc) {
* If newDoc is an array of documents, this will insert all documents in the cache * If newDoc is an array of documents, this will insert all documents in the cache
* @api private * @api private
*/ */
Datastore.prototype._insertInCache = function (newDoc) { Datastore.prototype._insertInCache = function (preparedDoc) {
if (util.isArray(newDoc)) { if (util.isArray(preparedDoc)) {
this._insertMultipleDocsInCache(newDoc); this._insertMultipleDocsInCache(preparedDoc);
} else { } else {
this.addToIndexes(this.prepareDocumentForInsertion(newDoc)); this.addToIndexes(preparedDoc);
} }
}; };
@ -347,10 +350,8 @@ Datastore.prototype._insertInCache = function (newDoc) {
* inserts and throws the error * inserts and throws the error
* @api private * @api private
*/ */
Datastore.prototype._insertMultipleDocsInCache = function (newDocs) { Datastore.prototype._insertMultipleDocsInCache = function (preparedDocs) {
var i, failingI, error var i, failingI, error;
, preparedDocs = this.prepareDocumentForInsertion(newDocs)
;
for (i = 0; i < preparedDocs.length; i += 1) { for (i = 0; i < preparedDocs.length; i += 1) {
try { try {

@ -54,44 +54,44 @@ describe('Database', function () {
}); });
describe('Autoloading', function () { describe('Autoloading', function () {
it('Can autoload a database and query it right away', function (done) { it('Can autoload a database and query it right away', function (done) {
var fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ _id: '2', a: 5, planet: 'Mars' }) + '\n' var fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ _id: '2', a: 5, planet: 'Mars' }) + '\n'
, autoDb = 'workspace/auto.db' , autoDb = 'workspace/auto.db'
, db , db
; ;
fs.writeFileSync(autoDb, fileStr, 'utf8'); fs.writeFileSync(autoDb, fileStr, 'utf8');
db = new Datastore({ filename: autoDb, autoload: true }) db = new Datastore({ filename: autoDb, autoload: true })
db.find({}, function (err, docs) { db.find({}, function (err, docs) {
assert.isNull(err); assert.isNull(err);
docs.length.should.equal(2); docs.length.should.equal(2);
done(); done();
}); });
}); });
it('Throws if autoload fails', function (done) { it('Throws if autoload fails', function (done) {
var fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ _id: '2', a: 5, planet: 'Mars' }) + '\n' + '{"$$indexCreated":{"fieldName":"a","unique":true}}' var fileStr = model.serialize({ _id: '1', a: 5, planet: 'Earth' }) + '\n' + model.serialize({ _id: '2', a: 5, planet: 'Mars' }) + '\n' + '{"$$indexCreated":{"fieldName":"a","unique":true}}'
, autoDb = 'workspace/auto.db' , autoDb = 'workspace/auto.db'
, db , db
; ;
fs.writeFileSync(autoDb, fileStr, 'utf8'); fs.writeFileSync(autoDb, fileStr, 'utf8');
// Check the loadDatabase generated an error // Check the loadDatabase generated an error
function onload (err) { function onload (err) {
err.errorType.should.equal('uniqueViolated'); err.errorType.should.equal('uniqueViolated');
done(); done();
} }
db = new Datastore({ filename: autoDb, autoload: true, onload: onload }) db = new Datastore({ filename: autoDb, autoload: true, onload: onload })
db.find({}, function (err, docs) { db.find({}, function (err, docs) {
done("Find should not be executed since autoload failed"); done("Find should not be executed since autoload failed");
}); });
}); });
}); });
describe('Insert', function () { describe('Insert', function () {
@ -207,7 +207,7 @@ describe('Database', function () {
d.insert({ _id: 'test', otherstuff: 42 }, function (err) { d.insert({ _id: 'test', otherstuff: 42 }, function (err) {
err.errorType.should.equal('uniqueViolated'); err.errorType.should.equal('uniqueViolated');
done(); done();
}); });
}); });
@ -223,18 +223,18 @@ describe('Database', function () {
}); });
}); });
}); });
it('Can insert an array of documents at once', function (done) { it('Can insert an array of documents at once', function (done) {
var docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }]; var docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }];
d.insert(docs, function (err) { d.insert(docs, function (err) {
d.find({}, function (err, docs) { d.find({}, function (err, docs) {
var data; var data;
docs.length.should.equal(2); docs.length.should.equal(2);
_.find(docs, function (doc) { return doc.a === 5; }).b.should.equal('hello'); _.find(docs, function (doc) { return doc.a === 5; }).b.should.equal('hello');
_.find(docs, function (doc) { return doc.a === 42; }).b.should.equal('world'); _.find(docs, function (doc) { return doc.a === 42; }).b.should.equal('world');
// The data has been persisted correctly // The data has been persisted correctly
data = _.filter(fs.readFileSync(testDb, 'utf8').split('\n'), function (line) { return line.length > 0; }); data = _.filter(fs.readFileSync(testDb, 'utf8').split('\n'), function (line) { return line.length > 0; });
data.length.should.equal(2); data.length.should.equal(2);
@ -242,19 +242,19 @@ describe('Database', function () {
model.deserialize(data[0]).b.should.equal('hello'); model.deserialize(data[0]).b.should.equal('hello');
model.deserialize(data[1]).a.should.equal(42); model.deserialize(data[1]).a.should.equal(42);
model.deserialize(data[1]).b.should.equal('world'); model.deserialize(data[1]).b.should.equal('world');
done(); done();
}); });
}); });
}); });
it('If a bulk insert violates a constraint, all changes are rolled back', function (done) { it('If a bulk insert violates a constraint, all changes are rolled back', function (done) {
var docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }, { a: 5, b: 'bloup' }, { a: 7 }]; var docs = [{ a: 5, b: 'hello' }, { a: 42, b: 'world' }, { a: 5, b: 'bloup' }, { a: 7 }];
d.ensureIndex({ fieldName: 'a', unique: true }, function () { // Important to specify callback here to make sure filesystem synced d.ensureIndex({ fieldName: 'a', unique: true }, function () { // Important to specify callback here to make sure filesystem synced
d.insert(docs, function (err) { d.insert(docs, function (err) {
err.errorType.should.equal('uniqueViolated'); err.errorType.should.equal('uniqueViolated');
d.find({}, function (err, docs) { d.find({}, function (err, docs) {
// Datafile only contains index definition // Datafile only contains index definition
var datafileContents = model.deserialize(fs.readFileSync(testDb, 'utf8')); var datafileContents = model.deserialize(fs.readFileSync(testDb, 'utf8'));
@ -265,10 +265,48 @@ describe('Database', function () {
done(); done();
}); });
}); });
});
});
it("If timestampData option is set, a createdAt field is added and persisted", function (done) {
var newDoc = { hello: 'world' }, beginning = Date.now();
d = new Datastore({ filename: testDb, timestampData: true, autoload: true });
d.find({}, function (err, docs) {
assert.isNull(err);
docs.length.should.equal(0);
d.insert(newDoc, function (err, insertedDoc) {
// No side effect on given input
assert.deepEqual(newDoc, { hello: 'world' });
// Insert doc has two new fields, _id and createdAt
insertedDoc.hello.should.equal('world');
assert.isDefined(insertedDoc.createdAt);
assert.isDefined(insertedDoc._id);
Object.keys(insertedDoc).length.should.equal(3);
assert.isBelow(Math.abs(insertedDoc.createdAt.getTime() - beginning), 15); // No more than 15ms should have elapsed
// Modifying results of insert doesn't change the cache
insertedDoc.bloup = "another";
Object.keys(insertedDoc).length.should.equal(4);
d.find({}, function (err, docs) {
docs.length.should.equal(1);
assert.deepEqual(newDoc, { hello: 'world' });
assert.deepEqual({ hello: 'world', _id: insertedDoc._id, createdAt: insertedDoc.createdAt }, docs[0]);
// All data correctly persisted on disk
d.loadDatabase(function () {
d.find({}, function (err, docs) {
docs.length.should.equal(1);
assert.deepEqual(newDoc, { hello: 'world' });
assert.deepEqual({ hello: 'world', _id: insertedDoc._id, createdAt: insertedDoc.createdAt }, docs[0]);
done();
});
});
});
});
}); });
}); });
it('Can insert a doc with id 0', function (done) { it('Can insert a doc with id 0', function (done) {

Loading…
Cancel
Save