Change#getModel(), Doc cleanup, Conflict event

This commit is contained in:
Ritchie Martori 2014-05-14 09:20:04 -07:00
parent 4bab42478f
commit 1e2ad9fba9
2 changed files with 166 additions and 40 deletions

View File

@ -66,6 +66,7 @@ Change.Conflict = Conflict;
*/ */
Change.setup = function() { Change.setup = function() {
DataModel.setup.call(this);
var Change = this; var Change = this;
Change.getter.id = function() { Change.getter.id = function() {
@ -271,7 +272,9 @@ Change.prototype.getModelCtor = function() {
Change.prototype.equals = function(change) { Change.prototype.equals = function(change) {
if(!change) return false; 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() { Change.prototype.getModelId = function() {
// TODO(ritch) get rid of the need to create an instance // 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 id = this.modelId;
var m = new Model(); var m = new Model();
m.setId(id); m.setId(id);
return m.getId(); 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. * When two changes conflict a conflict is created.
* *
* **Note: call `conflict.fetch()` to get the `target` and `source` models. * **Note: call `conflict.fetch()` to get the `target` and `source` models.
* *
* @param {Change} sourceChange The change object for the source model * @param {*} sourceModelId
* @param {Change} targetChange The conflicting model's change object * @param {*} targetModelId
* @property {Model} source The source model instance * @property {ModelClass} source The source model instance
* @property {Model} target The target model instance * @property {ModelClass} target The target model instance
*/ */
function Conflict(sourceChange, targetChange) { function Conflict(modelId, SourceModel, TargetModel) {
this.sourceChange = sourceChange; this.SourceModel = SourceModel;
this.targetChange = targetChange; 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 conflict = this;
var tasks = [ var SourceModel = this.SourceModel;
var TargetModel = this.TargetModel;
var source;
var target;
async.parallel([
getSourceModel, getSourceModel,
getTargetModel getTargetModel
]; ], done);
async.parallel(tasks, cb); function getSourceModel(cb) {
SourceModel.findById(conflict.modelId, function(err, model) {
function getSourceModel(change, cb) {
conflict.sourceModel.getModel(function(err, model) {
if(err) return cb(err); if(err) return cb(err);
conflict.source = model; source = model;
cb(); cb();
}); });
} }
function getTargetModel(cb) { function getTargetModel(cb) {
conflict.targetModel.getModel(function(err, model) { TargetModel.findById(conflict.modelId, function(err, model) {
if(err) return cb(err); if(err) return cb(err);
conflict.target = model; target = model;
cb(); cb();
}); });
} }
function done(err) {
if(err) return cb(err);
cb(null, source, target);
}
} }
Conflict.prototype.resolve = function(cb) { /**
this.sourceChange.prev = this.targetChange.rev; * Get the conflicting changes.
this.sourceChange.save(cb); *
* @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);
});
} }

View File

@ -44,7 +44,6 @@ DataModel.setup = function setupDataModel() {
return val ? new DataModel(val) : val; return val ? new DataModel(val) : val;
}); });
// enable change tracking (usually for replication) // enable change tracking (usually for replication)
if(this.settings.trackChanges) { if(this.settings.trackChanges) {
DataModel._defineChangeModel(); DataModel._defineChangeModel();
@ -424,6 +423,8 @@ DataModel.setupRemoting = function() {
var typeName = DataModel.modelName; var typeName = DataModel.modelName;
var options = DataModel.settings; var options = DataModel.settings;
// TODO(ritch) setRemoting should create its own function...
setRemoting(DataModel.create, { setRemoting(DataModel.create, {
description: 'Create a new instance of the model and persist it into the data source', 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'}}, accepts: {arg: 'data', type: 'object', description: 'Model instance data', http: {source: 'body'}},
@ -592,6 +593,11 @@ DataModel.changes = function(since, filter, callback) {
callback = since; callback = since;
since = -1; since = -1;
} }
if(typeof filter === 'function') {
callback = filter;
since = -1;
filter = {};
}
var idName = this.dataSource.idName(this.modelName); var idName = this.dataSource.idName(this.modelName);
var Change = this.getChangeModel(); var Change = this.getChangeModel();
@ -699,28 +705,16 @@ DataModel.replicate = function(since, targetModel, options, callback) {
} }
var tasks = [ var tasks = [
getLocalChanges, getSourceChanges,
getDiffFromTarget, getDiffFromTarget,
createSourceUpdates, createSourceUpdates,
bulkUpdate, bulkUpdate,
checkpoint checkpoint
]; ];
async.waterfall(tasks, function(err) { async.waterfall(tasks, done);
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);
});
callback && callback(null, conflicts); function getSourceChanges(cb) {
});
function getLocalChanges(cb) {
sourceModel.changes(since, options.filter, cb); sourceModel.changes(since, options.filter, cb);
} }
@ -735,7 +729,7 @@ DataModel.replicate = function(since, targetModel, options, callback) {
sourceModel.createUpdates(diff.deltas, cb); sourceModel.createUpdates(diff.deltas, cb);
} else { } else {
// nothing to replicate // nothing to replicate
callback(null, []); done();
} }
} }
@ -747,6 +741,22 @@ DataModel.replicate = function(since, targetModel, options, callback) {
var cb = arguments[arguments.length - 1]; var cb = arguments[arguments.length - 1];
sourceModel.checkpoint(cb); 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', return this.Change = BaseChangeModel.extend(this.modelName + '-change',
{}, {},
{ {
model: this trackModel: this
} }
); );
} }