diff --git a/lib/models/change.js b/lib/models/change.js index 410bab7a..c55db2d9 100644 --- a/lib/models/change.js +++ b/lib/models/change.js @@ -66,6 +66,7 @@ Change.Conflict = Conflict; */ Change.setup = function() { + DataModel.setup.call(this); var Change = this; Change.getter.id = function() { @@ -271,7 +272,9 @@ Change.prototype.getModelCtor = function() { Change.prototype.equals = function(change) { if(!change) return false; - return change.rev === this.rev; + var thisRev = this.rev || null; + var thatRev = change.rev || null; + return thisRev === thatRev; } /** @@ -402,56 +405,169 @@ Change.handleError = function(err) { Change.prototype.getModelId = function() { // TODO(ritch) get rid of the need to create an instance - var Model = this.constructor.settings.model; + var Model = this.constructor.settings.trackModel; var id = this.modelId; var m = new Model(); m.setId(id); return m.getId(); } +Change.prototype.getModel = function(callback) { + var Model = this.constructor.settings.trackModel; + var id = this.getModelId(); + Model.findById(id, callback); +} + /** * When two changes conflict a conflict is created. * * **Note: call `conflict.fetch()` to get the `target` and `source` models. * - * @param {Change} sourceChange The change object for the source model - * @param {Change} targetChange The conflicting model's change object - * @property {Model} source The source model instance - * @property {Model} target The target model instance + * @param {*} sourceModelId + * @param {*} targetModelId + * @property {ModelClass} source The source model instance + * @property {ModelClass} target The target model instance */ -function Conflict(sourceChange, targetChange) { - this.sourceChange = sourceChange; - this.targetChange = targetChange; +function Conflict(modelId, SourceModel, TargetModel) { + this.SourceModel = SourceModel; + this.TargetModel = TargetModel; + this.SourceChange = SourceModel.getChangeModel(); + this.TargetChange = TargetModel.getChangeModel(); + this.modelId = modelId; } -Conflict.prototype.fetch = function(cb) { +/** + * Fetch the conflicting models. + * + * @callback {Function} callback + * @param {Error} + * @param {DataModel} source + * @param {DataModel} target + */ + +Conflict.prototype.models = function(cb) { var conflict = this; - var tasks = [ + var SourceModel = this.SourceModel; + var TargetModel = this.TargetModel; + var source; + var target; + + async.parallel([ getSourceModel, getTargetModel - ]; + ], done); - async.parallel(tasks, cb); - - function getSourceModel(change, cb) { - conflict.sourceModel.getModel(function(err, model) { + function getSourceModel(cb) { + SourceModel.findById(conflict.modelId, function(err, model) { if(err) return cb(err); - conflict.source = model; + source = model; cb(); }); } function getTargetModel(cb) { - conflict.targetModel.getModel(function(err, model) { + TargetModel.findById(conflict.modelId, function(err, model) { if(err) return cb(err); - conflict.target = model; + target = model; cb(); }); } + + function done(err) { + if(err) return cb(err); + cb(null, source, target); + } } -Conflict.prototype.resolve = function(cb) { - this.sourceChange.prev = this.targetChange.rev; - this.sourceChange.save(cb); +/** + * Get the conflicting changes. + * + * @callback {Function} callback + * @param {Error} err + * @param {Change} sourceChange + * @param {Change} targetChange + */ + +Conflict.prototype.changes = function(cb) { + var conflict = this; + var sourceChange; + var targetChange; + + async.parallel([ + getSourceChange, + getTargetChange + ], done); + + function getSourceChange(cb) { + conflict.SourceChange.findOne({ + modelId: conflict.sourceModelId + }, function(err, change) { + if(err) return cb(err); + sourceChange = change; + cb(); + }); + } + + function getTargetChange(cb) { + debugger; + conflict.TargetChange.findOne({ + modelId: conflict.targetModelId + }, function(err, change) { + if(err) return cb(err); + targetChange = change; + cb(); + }); + } + + function done(err) { + if(err) return cb(err); + cb(null, sourceChange, targetChange); + } +} + +/** + * Resolve the conflict. + * + * @callback {Function} callback + * @param {Error} err + */ + +Conflict.prototype.resolve = function(cb) { + var conflict = this; + conflict.changes(function(err, sourceChange, targetChange) { + if(err) return callback(err); + sourceChange.prev = targetChange.rev; + sourceChange.save(cb); + }); +} + +/** + * Determine the conflict type. + * + * ```js + * // possible results are + * Change.UPDATE // => source and target models were updated + * Change.DELETE // => the source and or target model was deleted + * Change.UNKNOWN // => the conflict type is uknown or due to an error + * ``` + * @callback {Function} callback + * @param {Error} err + * @param {String} type The conflict type. + */ + +Conflict.prototype.type = function(cb) { + var conflict = this; + this.changes(function(err, sourceChange, targetChange) { + if(err) return cb(err); + var sourceChangeType = sourceChange.type(); + var targetChangeType = targetChange.type(); + if(sourceChangeType === Change.UPDATE && targetChangeType === Change.UPDATE) { + return cb(null, Change.UPDATE); + } + if(sourceChangeType === Change.DELETE || targetChangeType === Change.DELETE) { + return cb(null, Change.DELETE); + } + return cb(null, Change.UNKNOWN); + }); } diff --git a/lib/models/data-model.js b/lib/models/data-model.js index 7580c9ee..8295d6dd 100644 --- a/lib/models/data-model.js +++ b/lib/models/data-model.js @@ -44,7 +44,6 @@ DataModel.setup = function setupDataModel() { return val ? new DataModel(val) : val; }); - // enable change tracking (usually for replication) if(this.settings.trackChanges) { DataModel._defineChangeModel(); @@ -424,6 +423,8 @@ DataModel.setupRemoting = function() { var typeName = DataModel.modelName; var options = DataModel.settings; + // TODO(ritch) setRemoting should create its own function... + setRemoting(DataModel.create, { description: 'Create a new instance of the model and persist it into the data source', accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}}, @@ -592,6 +593,11 @@ DataModel.changes = function(since, filter, callback) { callback = since; since = -1; } + if(typeof filter === 'function') { + callback = filter; + since = -1; + filter = {}; + } var idName = this.dataSource.idName(this.modelName); var Change = this.getChangeModel(); @@ -699,28 +705,16 @@ DataModel.replicate = function(since, targetModel, options, callback) { } var tasks = [ - getLocalChanges, + getSourceChanges, getDiffFromTarget, createSourceUpdates, bulkUpdate, checkpoint ]; - async.waterfall(tasks, function(err) { - if(err) return callback(err); - var conflicts = diff.conflicts.map(function(change) { - var sourceChange = new Change({ - modelName: sourceModel.modelName, - modelId: change.modelId - }); - var targetChange = new TargetChange(change); - return new Change.Conflict(sourceChange, targetChange); - }); + async.waterfall(tasks, done); - callback && callback(null, conflicts); - }); - - function getLocalChanges(cb) { + function getSourceChanges(cb) { sourceModel.changes(since, options.filter, cb); } @@ -735,7 +729,7 @@ DataModel.replicate = function(since, targetModel, options, callback) { sourceModel.createUpdates(diff.deltas, cb); } else { // nothing to replicate - callback(null, []); + done(); } } @@ -747,6 +741,22 @@ DataModel.replicate = function(since, targetModel, options, callback) { var cb = arguments[arguments.length - 1]; sourceModel.checkpoint(cb); } + + function done(err) { + if(err) return callback(err); + + var conflicts = diff.conflicts.map(function(change) { + return new Change.Conflict( + change.modelId, sourceModel, targetModel + ); + }); + + if(conflicts.length) { + sourceModel.emit('conflicts', conflicts); + } + + callback && callback(null, conflicts); + } } /** @@ -924,7 +934,7 @@ DataModel._defineChangeModel = function() { return this.Change = BaseChangeModel.extend(this.modelName + '-change', {}, { - model: this + trackModel: this } ); }