Merge pull request #1233 from strongloop/feature/conflict-resolution-api
Add conflict resolution API
This commit is contained in:
commit
4a8c3be8f4
|
@ -616,6 +616,13 @@ module.exports = function(Change) {
|
||||||
/**
|
/**
|
||||||
* Resolve the conflict.
|
* Resolve the conflict.
|
||||||
*
|
*
|
||||||
|
* Set the source change's previous revision to the current revision of the
|
||||||
|
* (conflicting) target change. Since the changes are no longer conflicting
|
||||||
|
* and appear as if the source change was based on the target, they will be
|
||||||
|
* replicated normally as part of the next replicate() call.
|
||||||
|
*
|
||||||
|
* This is effectively resolving the conflict using the source version.
|
||||||
|
*
|
||||||
* @callback {Function} callback
|
* @callback {Function} callback
|
||||||
* @param {Error} err
|
* @param {Error} err
|
||||||
*/
|
*/
|
||||||
|
@ -629,6 +636,74 @@ module.exports = function(Change) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the conflict using the instance data in the source model.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
Conflict.prototype.resolveUsingSource = function(cb) {
|
||||||
|
this.resolve(function(err) {
|
||||||
|
// don't forward any cb arguments from resolve()
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the conflict using the instance data in the target model.
|
||||||
|
*
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
Conflict.prototype.resolveUsingTarget = function(cb) {
|
||||||
|
var conflict = this;
|
||||||
|
|
||||||
|
conflict.models(function(err, source, target) {
|
||||||
|
if (err) return done(err);
|
||||||
|
if (target === null) {
|
||||||
|
return conflict.SourceModel.deleteById(conflict.modelId, done);
|
||||||
|
}
|
||||||
|
var inst = new conflict.SourceModel(target);
|
||||||
|
inst.save(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
function done(err) {
|
||||||
|
// don't forward any cb arguments from internal calls
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the conflict using the supplied instance data.
|
||||||
|
*
|
||||||
|
* @param {Object} data The set of changes to apply on the model
|
||||||
|
* instance. Use `null` value to delete the source instance instead.
|
||||||
|
* @callback {Function} callback
|
||||||
|
* @param {Error} err
|
||||||
|
*/
|
||||||
|
|
||||||
|
Conflict.prototype.resolveManually = function(data, cb) {
|
||||||
|
var conflict = this;
|
||||||
|
if (!data) {
|
||||||
|
return conflict.SourceModel.deleteById(conflict.modelId, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
conflict.models(function(err, source, target) {
|
||||||
|
if (err) return done(err);
|
||||||
|
var inst = source || new conflict.SourceModel(target);
|
||||||
|
inst.setAttributes(data);
|
||||||
|
inst.save(function(err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
conflict.resolve(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function done(err) {
|
||||||
|
// don't forward any cb arguments from internal calls
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine the conflict type.
|
* Determine the conflict type.
|
||||||
*
|
*
|
||||||
|
|
|
@ -963,50 +963,70 @@ describe('Replication / Change APIs', function() {
|
||||||
], done);
|
], done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles conflict resolved using "ours"', function(done) {
|
it('handles UPDATE conflict resolved using "ours"', function(done) {
|
||||||
testResolvedConflictIsHandledWithNoMoreConflicts(
|
testUpdateConflictIsResolved(
|
||||||
function resolveUsingOurs(conflict, cb) {
|
function resolveUsingOurs(conflict, cb) {
|
||||||
conflict.resolve(cb);
|
conflict.resolveUsingSource(cb);
|
||||||
},
|
},
|
||||||
done);
|
done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles conflict resolved using "theirs"', function(done) {
|
it('handles UPDATE conflict resolved using "theirs"', function(done) {
|
||||||
testResolvedConflictIsHandledWithNoMoreConflicts(
|
testUpdateConflictIsResolved(
|
||||||
function resolveUsingTheirs(conflict, cb) {
|
function resolveUsingTheirs(conflict, cb) {
|
||||||
conflict.models(function(err, source, target) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
// We sync ClientA->Server first
|
// We sync ClientA->Server first
|
||||||
expect(conflict.SourceModel.modelName)
|
expect(conflict.SourceModel.modelName)
|
||||||
.to.equal(ClientB.modelName);
|
.to.equal(ClientB.modelName);
|
||||||
var m = new conflict.SourceModel(target);
|
conflict.resolveUsingTarget(cb);
|
||||||
m.save(cb);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
done);
|
done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles conflict resolved manually', function(done) {
|
it('handles UPDATE conflict resolved manually', function(done) {
|
||||||
testResolvedConflictIsHandledWithNoMoreConflicts(
|
testUpdateConflictIsResolved(
|
||||||
function resolveManually(conflict, cb) {
|
function resolveManually(conflict, cb) {
|
||||||
conflict.models(function(err, source, target) {
|
conflict.resolveManually({ name: 'manual' }, cb);
|
||||||
if (err) return cb(err);
|
},
|
||||||
var m = source || new conflict.SourceModel(target);
|
done);
|
||||||
m.name = 'manual';
|
|
||||||
m.save(function(err) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
conflict.resolve(function(err) {
|
|
||||||
if (err) return cb(err);
|
|
||||||
cb();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles DELETE conflict resolved using "ours"', function(done) {
|
||||||
|
testDeleteConflictIsResolved(
|
||||||
|
function resolveUsingOurs(conflict, cb) {
|
||||||
|
conflict.resolveUsingSource(cb);
|
||||||
|
},
|
||||||
|
done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles DELETE conflict resolved using "theirs"', function(done) {
|
||||||
|
testDeleteConflictIsResolved(
|
||||||
|
function resolveUsingTheirs(conflict, cb) {
|
||||||
|
// We sync ClientA->Server first
|
||||||
|
expect(conflict.SourceModel.modelName)
|
||||||
|
.to.equal(ClientB.modelName);
|
||||||
|
conflict.resolveUsingTarget(cb);
|
||||||
|
},
|
||||||
|
done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles DELETE conflict resolved as manual delete', function(done) {
|
||||||
|
testDeleteConflictIsResolved(
|
||||||
|
function resolveManually(conflict, cb) {
|
||||||
|
conflict.resolveManually(null, cb);
|
||||||
|
},
|
||||||
|
done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles DELETE conflict resolved manually', function(done) {
|
||||||
|
testDeleteConflictIsResolved(
|
||||||
|
function resolveManually(conflict, cb) {
|
||||||
|
conflict.resolveManually({ name: 'manual' }, cb);
|
||||||
},
|
},
|
||||||
done);
|
done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function testResolvedConflictIsHandledWithNoMoreConflicts(resolver, cb) {
|
function testUpdateConflictIsResolved(resolver, cb) {
|
||||||
async.series([
|
async.series([
|
||||||
// sync the new model to ClientB
|
// sync the new model to ClientB
|
||||||
sync(ClientB, Server),
|
sync(ClientB, Server),
|
||||||
|
@ -1041,6 +1061,44 @@ describe('Replication / Change APIs', function() {
|
||||||
], cb);
|
], cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testDeleteConflictIsResolved(resolver, cb) {
|
||||||
|
async.series([
|
||||||
|
// sync the new model to ClientB
|
||||||
|
sync(ClientB, Server),
|
||||||
|
verifyInstanceWasReplicated(ClientA, ClientB, sourceInstanceId),
|
||||||
|
|
||||||
|
// ClientA makes a change
|
||||||
|
function deleteInstanceOnClientA(next) {
|
||||||
|
ClientA.deleteById(sourceInstanceId, next);
|
||||||
|
},
|
||||||
|
|
||||||
|
sync(ClientA, Server),
|
||||||
|
|
||||||
|
// ClientB changes the same instance
|
||||||
|
updateClientB('b'),
|
||||||
|
|
||||||
|
function syncAndResolveConflict(next) {
|
||||||
|
replicate(ClientB, Server, function(err, conflicts, cps) {
|
||||||
|
if (err) return next(err);
|
||||||
|
|
||||||
|
expect(conflicts).to.have.length(1);
|
||||||
|
expect(conflicts[0].SourceModel.modelName)
|
||||||
|
.to.equal(ClientB.modelName);
|
||||||
|
|
||||||
|
debug('Resolving the conflict %j', conflicts[0]);
|
||||||
|
resolver(conflicts[0], next);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// repeat the last sync, it should pass now
|
||||||
|
sync(ClientB, Server),
|
||||||
|
// and sync back to ClientA too
|
||||||
|
sync(ClientA, Server),
|
||||||
|
|
||||||
|
verifyInstanceWasReplicated(ClientB, ClientA, sourceInstanceId)
|
||||||
|
], cb);
|
||||||
|
}
|
||||||
|
|
||||||
function updateClientB(name) {
|
function updateClientB(name) {
|
||||||
return function updateInstanceB(next) {
|
return function updateInstanceB(next) {
|
||||||
ClientB.findById(sourceInstanceId, function(err, instance) {
|
ClientB.findById(sourceInstanceId, function(err, instance) {
|
||||||
|
|
Loading…
Reference in New Issue