diff --git a/lib/datastore.js b/lib/datastore.js index 0c4d53c..3103756 100644 --- a/lib/datastore.js +++ b/lib/datastore.js @@ -18,6 +18,7 @@ var customUtils = require('./customUtils') * Node Webkit stores application data such as cookies and local storage (the best place to store data in my opinion) * @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.afterSerialization and options.beforeDeserialization Optional, serialization hooks */ function Datastore (options) { var filename; @@ -42,7 +43,10 @@ function Datastore (options) { } // Persistence handling - this.persistence = new Persistence({ db: this, nodeWebkitAppName: options.nodeWebkitAppName }); + this.persistence = new Persistence({ db: this, nodeWebkitAppName: options.nodeWebkitAppName + , afterSerialization: options.afterSerialization + , beforeDeserialization: options.beforeDeserialization + }); // This new executor is ready if we don't use persistence // If we do, it will only be ready once loadDatabase is called diff --git a/lib/persistence.js b/lib/persistence.js index e46ab3f..048aa19 100644 --- a/lib/persistence.js +++ b/lib/persistence.js @@ -34,6 +34,10 @@ function Persistence (options) { } } + // After serialization and before deserialization hooks + this.afterSerialization = options.afterSerialization || function (s) { return s; }; + this.beforeDeserialization = options.beforeDeserialization || function (s) { return s; }; + // For NW apps, store data in the same directory where NW stores application data if (this.filename && options.nodeWebkitAppName) { console.log("=================================================================="); @@ -119,11 +123,11 @@ Persistence.prototype.persistCachedDatabase = function (cb) { if (this.inMemoryOnly) { return callback(null); } this.db.getAllData().forEach(function (doc) { - toPersist += model.serialize(doc) + '\n'; + toPersist += self.afterSerialization(model.serialize(doc)) + '\n'; }); Object.keys(this.db.indexes).forEach(function (fieldName) { if (fieldName != "_id") { // The special _id index is managed by datastore.js, the others need to be persisted - toPersist += model.serialize({ $$indexCreated: { fieldName: fieldName, unique: self.db.indexes[fieldName].unique, sparse: self.db.indexes[fieldName].sparse }}) + '\n'; + toPersist += self.afterSerialization(model.serialize({ $$indexCreated: { fieldName: fieldName, unique: self.db.indexes[fieldName].unique, sparse: self.db.indexes[fieldName].sparse }})) + '\n'; } }); @@ -200,7 +204,7 @@ Persistence.prototype.persistNewState = function (newDocs, cb) { if (self.inMemoryOnly) { return callback(null); } newDocs.forEach(function (doc) { - toPersist += model.serialize(doc) + '\n'; + toPersist += self.afterSerialization(model.serialize(doc)) + '\n'; }); if (toPersist.length === 0) { return callback(null); } @@ -215,7 +219,7 @@ Persistence.prototype.persistNewState = function (newDocs, cb) { * From a database's raw data, return the corresponding * machine understandable collection */ -Persistence.treatRawData = function (rawData) { +Persistence.prototype.treatRawData = function (rawData) { var data = rawData.split('\n') , dataById = {} , tdata = [] @@ -227,7 +231,7 @@ Persistence.treatRawData = function (rawData) { var doc; try { - doc = model.deserialize(data[i]); + doc = model.deserialize(this.beforeDeserialization(data[i])); if (doc._id) { if (doc.$$deleted === true) { delete dataById[doc._id]; @@ -302,7 +306,7 @@ Persistence.prototype.loadDatabase = function (cb) { storage.readFile(self.filename, 'utf8', function (err, rawData) { if (err) { return cb(err); } - var treatedData = Persistence.treatRawData(rawData); + var treatedData = self.treatRawData(rawData); // Recreate all indexes in the datafile Object.keys(treatedData.indexes).forEach(function (key) { diff --git a/test/persistence.test.js b/test/persistence.test.js index b98bab8..3fbb41f 100644 --- a/test/persistence.test.js +++ b/test/persistence.test.js @@ -46,7 +46,7 @@ describe('Persistence', function () { , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + model.serialize({ _id: "2", hello: 'world' }) + '\n' + model.serialize({ _id: "3", nested: { today: now } }) - , treatedData = Persistence.treatRawData(rawData).data + , treatedData = d.persistence.treatRawData(rawData).data ; treatedData.sort(function (a, b) { return a._id - b._id; }); @@ -61,7 +61,7 @@ describe('Persistence', function () { , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + 'garbage\n' + model.serialize({ _id: "3", nested: { today: now } }) - , treatedData = Persistence.treatRawData(rawData).data + , treatedData = d.persistence.treatRawData(rawData).data ; treatedData.sort(function (a, b) { return a._id - b._id; }); @@ -75,7 +75,7 @@ describe('Persistence', function () { , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + model.serialize({ _id: "2", hello: 'world' }) + '\n' + model.serialize({ nested: { today: now } }) - , treatedData = Persistence.treatRawData(rawData).data + , treatedData = d.persistence.treatRawData(rawData).data ; treatedData.sort(function (a, b) { return a._id - b._id; }); @@ -89,7 +89,7 @@ describe('Persistence', function () { , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + model.serialize({ _id: "2", hello: 'world' }) + '\n' + model.serialize({ _id: "1", nested: { today: now } }) - , treatedData = Persistence.treatRawData(rawData).data + , treatedData = d.persistence.treatRawData(rawData).data ; treatedData.sort(function (a, b) { return a._id - b._id; }); @@ -104,7 +104,7 @@ describe('Persistence', function () { model.serialize({ _id: "2", hello: 'world' }) + '\n' + model.serialize({ _id: "1", $$deleted: true }) + '\n' + model.serialize({ _id: "3", today: now }) - , treatedData = Persistence.treatRawData(rawData).data + , treatedData = d.persistence.treatRawData(rawData).data ; treatedData.sort(function (a, b) { return a._id - b._id; }); @@ -118,7 +118,7 @@ describe('Persistence', function () { , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + model.serialize({ _id: "2", $$deleted: true }) + '\n' + model.serialize({ _id: "3", today: now }) - , treatedData = Persistence.treatRawData(rawData).data + , treatedData = d.persistence.treatRawData(rawData).data ; treatedData.sort(function (a, b) { return a._id - b._id; }); @@ -132,8 +132,8 @@ describe('Persistence', function () { , rawData = model.serialize({ _id: "1", a: 2, ages: [1, 5, 12] }) + '\n' + model.serialize({ $$indexCreated: { fieldName: "test", unique: true } }) + '\n' + model.serialize({ _id: "3", today: now }) - , treatedData = Persistence.treatRawData(rawData).data - , indexes = Persistence.treatRawData(rawData).indexes + , treatedData = d.persistence.treatRawData(rawData).data + , indexes = d.persistence.treatRawData(rawData).indexes ; Object.keys(indexes).length.should.equal(1); @@ -268,6 +268,86 @@ describe('Persistence', function () { }); }); + describe.only('Data can be persisted using serialization hooks', function () { + + it("A serialization hook can be used to transform data before writing new state to disk", function (done) { + var as = function (s) { return "before_" + s + "_after"; } + , hookTestFilename = 'workspace/hookTest.db' + , d = new Datastore({ filename: hookTestFilename, autoload: true + , afterSerialization: as + }) + ; + + d.insert({ hello: "world" }, function () { + var _data = fs.readFileSync(hookTestFilename, 'utf8') + , data = _data.split('\n') + , doc0 = data[0].substring(7, data[0].length - 6) + ; + + data.length.should.equal(2); + + data[0].substring(0, 7).should.equal('before_'); + data[0].substring(data[0].length - 6).should.equal('_after'); + + doc0 = model.deserialize(doc0); + Object.keys(doc0).length.should.equal(2); + doc0.hello.should.equal('world'); + + d.insert({ p: 'Mars' }, function () { + var _data = fs.readFileSync(hookTestFilename, 'utf8') + , data = _data.split('\n') + , doc0 = data[0].substring(7, data[0].length - 6) + , doc1 = data[1].substring(7, data[1].length - 6) + ; + + data.length.should.equal(3); + + data[0].substring(0, 7).should.equal('before_'); + data[0].substring(data[0].length - 6).should.equal('_after'); + data[1].substring(0, 7).should.equal('before_'); + data[1].substring(data[1].length - 6).should.equal('_after'); + + doc0 = model.deserialize(doc0); + Object.keys(doc0).length.should.equal(2); + doc0.hello.should.equal('world'); + + doc1 = model.deserialize(doc1); + Object.keys(doc1).length.should.equal(2); + doc1.p.should.equal('Mars'); + + d.ensureIndex({ fieldName: 'idefix' }, function () { + var _data = fs.readFileSync(hookTestFilename, 'utf8') + , data = _data.split('\n') + , doc0 = data[0].substring(7, data[0].length - 6) + , doc1 = data[1].substring(7, data[1].length - 6) + , idx = data[2].substring(7, data[2].length - 6) + ; + + data.length.should.equal(4); + + data[0].substring(0, 7).should.equal('before_'); + data[0].substring(data[0].length - 6).should.equal('_after'); + data[1].substring(0, 7).should.equal('before_'); + data[1].substring(data[1].length - 6).should.equal('_after'); + + doc0 = model.deserialize(doc0); + Object.keys(doc0).length.should.equal(2); + doc0.hello.should.equal('world'); + + doc1 = model.deserialize(doc1); + Object.keys(doc1).length.should.equal(2); + doc1.p.should.equal('Mars'); + + idx = model.deserialize(idx); + assert.deepEqual(idx, { '$$indexCreated': { fieldName: 'idefix' } }); + + done(); + }); + }); + }); + }); + + }); describe('Prevent dataloss when persisting data', function () {