diff --git a/lib/models/model.js b/lib/models/model.js index 8c08d5d0..8cc25832 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -338,6 +338,20 @@ Model.checkpoint = function(cb) { }); } +/** + * Get the current checkpoint id. + * + * @callback {Function} callback + * @param {Error} err + * @param {Number} currentCheckpointId + * @end + */ + +Model.currentCheckpoint = function(cb) { + var Checkpoint = this.getChangeModel().getCheckpointModel(); + Checkpoint.current(cb); +} + /** * Replicate changes since the given checkpoint to the given target model. * @@ -489,6 +503,12 @@ Model.bulkUpdate = function(updates, callback) { async.parallel(tasks, callback); } +/** + * Get the `Change` model. + * + * @return {Change} + */ + Model.getChangeModel = function() { var changeModel = this.Change; if(changeModel) return changeModel; @@ -497,8 +517,25 @@ Model.getChangeModel = function() { return changeModel; } +/** + * Get the source identifier for this model / dataSource. + * + * @callback {Function} callback + * @param {Error} err + * @param {String} sourceId + */ + Model.getSourceId = function(cb) { - cb(null, 'foo') + var dataSource = this.dataSource; + if(!dataSource) { + this.once('dataSourceAttached', this.getSourceId.bind(this, cb)); + } + assert( + dataSource.connector.name, + 'Model.getSourceId: cannot get id without dataSource.connector.name' + ); + var id = [dataSource.connector.name, this.modelName].join('-'); + cb(null, id); } /** diff --git a/test/change.test.js b/test/change.test.js index 3abebd38..358fbbfa 100644 --- a/test/change.test.js +++ b/test/change.test.js @@ -253,8 +253,11 @@ describe('Change', function(){ it('should return delta and conflict lists', function (done) { var remoteChanges = [ + // an update => should result in a delta {rev: 'foo2', prev: 'foo', modelName: this.modelName, modelId: 9, checkpoint: 1}, + // no change => should not result in a delta / conflict {rev: 'bar', prev: 'bar', modelName: this.modelName, modelId: 10, checkpoint: 1}, + // a conflict => should result in a conflict {rev: 'bat2', prev: 'bat0', modelName: this.modelName, modelId: 11, checkpoint: 1}, ]; diff --git a/test/model.test.js b/test/model.test.js index fe74b62b..ca8e4999 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1,4 +1,7 @@ -var ACL = require('../').ACL; +var async = require('async'); +var loopback = require('../'); +var ACL = loopback.ACL; +var Change = loopback.Change; describe('Model', function() { @@ -523,33 +526,33 @@ describe('Model', function() { describe('Model.extend() events', function() { it('create isolated emitters for subclasses', function() { - var User1 = loopback.createModel('User1', { - 'first': String, - 'last': String - }); + var User1 = loopback.createModel('User1', { + 'first': String, + 'last': String + }); - var User2 = loopback.createModel('User2', { - 'name': String - }); + var User2 = loopback.createModel('User2', { + 'name': String + }); - var user1Triggered = false; - User1.once('x', function(event) { - user1Triggered = true; - }); + var user1Triggered = false; + User1.once('x', function(event) { + user1Triggered = true; + }); - var user2Triggered = false; - User2.once('x', function(event) { - user2Triggered = true; - }); + var user2Triggered = false; + User2.once('x', function(event) { + user2Triggered = true; + }); - assert(User1.once !== User2.once); - assert(User1.once !== loopback.Model.once); + assert(User1.once !== User2.once); + assert(User1.once !== loopback.Model.once); - User1.emit('x', User1); + User1.emit('x', User1); - assert(user1Triggered); - assert(!user2Triggered); + assert(user1Triggered); + assert(!user2Triggered); }); }); @@ -581,80 +584,125 @@ describe('Model', function() { } }); - // describe('Model.hasAndBelongsToMany()', function() { - // it("TODO: implement / document", function(done) { - // /* example - - // - // */ - // done(new Error('test not implemented')); - // }); - // }); + describe('Model.getChangeModel()', function() { + it('Get the Change Model', function () { + var UserChange = User.getChangeModel(); + var change = new UserChange(); + assert(change instanceof Change); + }); + }); - // describe('Model.remoteMethods()', function() { - // it("Return a list of enabled remote methods", function() { - // app.model(User); - // User.remoteMethods(); // ['save', ...] - // }); - // }); + describe('Model.getSourceId(callback)', function() { + it('Get the Source Id', function (done) { + User.getSourceId(function(err, id) { + assert.equal('memory-user', id); + done(); + }); + }); + }); - // describe('Model.availableMethods()', function() { - // it("Returns the currently available api of a model as well as descriptions of any modified behavior or methods from attached data sources", function(done) { - // /* example - - // User.attachTo(oracle); - // console.log(User.availableMethods()); - // - // { - // 'User.all': { - // accepts: [{arg: 'filter', type: 'object', description: '...'}], - // returns: [{arg: 'users', type: ['User']}] - // }, - // 'User.find': { - // accepts: [{arg: 'id', type: 'any'}], - // returns: [{arg: 'items', type: 'User'}] - // }, - // ... - // } - // var oracle = loopback.createDataSource({ - // connector: 'oracle', - // host: '111.22.333.44', - // database: 'MYDB', - // username: 'username', - // password: 'password' - // }); - // - // */ - // done(new Error('test not implemented')); - // }); - // }); - -// describe('Model.before(name, fn)', function(){ -// it('Run a function before a method is called', function() { -// // User.before('save', function(user, next) { -// // console.log('about to save', user); -// // -// // next(); -// // }); -// // -// // User.before('delete', function(user, next) { -// // // prevent all delete calls -// // next(new Error('deleting is disabled')); -// // }); -// // User.beforeRemote('save', function(ctx, user, next) { -// // if(ctx.user.id === user.id) { -// // next(); -// // } else { -// // next(new Error('must be logged in to update')) -// // } -// // }); -// -// throw new Error('not implemented'); -// }); -// }); -// -// describe('Model.after(name, fn)', function(){ -// it('Run a function after a method is called', function() { -// -// throw new Error('not implemented'); -// }); -// }); + describe('Model.checkpoint(callback)', function() { + it('Create a checkpoint', function (done) { + var Checkpoint = User.getChangeModel().getCheckpointModel(); + var tasks = [ + getCurrentCheckpoint, + checkpoint + ]; + var result; + var current; + + async.parallel(tasks, function(err) { + if(err) return done(err); + + assert.equal(result, current + 1); + done(); + }); + + function getCurrentCheckpoint(cb) { + Checkpoint.current(function(err, cp) { + current = cp; + cb(err); + }); + } + + function checkpoint(cb) { + User.checkpoint(function(err, cp) { + result = cp.id; + cb(err); + }); + } + }); + }); + + describe('Replication / Change APIs', function() { + beforeEach(function(done) { + var test = this; + this.dataSource = loopback.createDataSource({connector: loopback.Memory}); + var SourceModel = this.SourceModel = this.dataSource.createModel('SourceModel', {}, { + trackChanges: true + }); + var TargetModel = this.TargetModel = this.dataSource.createModel('TargetModel', {}, { + trackChanges: true + }); + + var createOne = SourceModel.create.bind(SourceModel, { + name: 'baz' + }); + + async.parallel([ + createOne, + function(cb) { + SourceModel.currentCheckpoint(function(err, id) { + if(err) return cb(err); + test.startingCheckpoint = id; + cb(); + }); + } + ], process.nextTick.bind(process, done)); + }); + + describe('Model.changes(since, filter, callback)', function() { + it('Get changes since the given checkpoint', function (done) { + this.SourceModel.changes(this.startingCheckpoint, {}, function(err, changes) { + assert.equal(changes.length, 1); + done(); + }); + }); + }); + + describe('Model.replicate(since, targetModel, options, callback)', function() { + it('Replicate data using the target model', function (done) { + var test = this; + var options = {}; + var sourceData; + var targetData; + + this.SourceModel.replicate(this.startingCheckpoint, this.TargetModel, + options, function(err, conflicts) { + assert(conflicts.length === 0); + async.parallel([ + function(cb) { + test.SourceModel.find(function(err, result) { + if(err) return cb(err); + sourceData = result; + cb(); + }); + }, + function(cb) { + test.TargetModel.find(function(err, result) { + if(err) return cb(err); + targetData = result; + cb(); + }); + } + ], function(err) { + if(err) return done(err); + + assert.deepEqual(sourceData, targetData); + done(); + }); + }); + }); + }); + }); });