feat(user): resolve conflicts

This commit is contained in:
Geoff Cox 2017-09-05 14:15:49 -07:00
parent 72ea73bbd2
commit ddaa14bb0e
3 changed files with 119 additions and 7 deletions

View file

@ -75,10 +75,11 @@ Doc.prototype.updateIgnoreConflict = function (dbName, doc) {
});
};
Doc.prototype.get = function (dbName, docId) {
Doc.prototype.get = function (dbName, docId, params) {
return promisedRequest.request({
uri: this._slouch._url + '/' + dbName + '/' + docId,
method: 'GET'
method: 'GET',
qs: params
}, true);
};

View file

@ -3,7 +3,8 @@
var NotAuthenticatedError = require('./not-authenticated-error'),
request = require('./request'),
url = require('url'),
sporks = require('sporks');
sporks = require('sporks'),
Promise = require('sporks/scripts/promise');
var User = function (slouch) {
this._slouch = slouch;
@ -37,8 +38,8 @@ User.prototype.create = function (username, password, roles, metadata) {
return this._insert(username, user);
};
User.prototype.get = function (username) {
return this._slouch.doc.get(this._dbName, this.toUserId(username));
User.prototype.get = function (username, params) {
return this._slouch.doc.get(this._dbName, this.toUserId(username), params);
};
User.prototype._update = function (username, user) {
@ -159,4 +160,48 @@ User.prototype.authenticateAndGetSession = function (username, password) {
});
};
// Provides a simple way of resolving conflicts at the user layer whereby a merge of the roles is
// assumed to be the only data needed in the conflicting docs. Until you resolve these conflicts,
// users cannot log in. You can pretty easily encounter conflicts on user docs with CouchDB 2. For
// example, if a user is being added to two roles simultaneously via different CouchDB nodes then
// when the CouchDB nodes replicate the user, the user will be in conflict.
User.prototype.resolveConflicts = function (username) {
var self = this;
return self.get(username, {
conflicts: true
}).then(function (user) {
// Verify that there is a conflict
if (user._conflicts) {
var roles = sporks.flip(user.roles),
gets = [],
destroys = [];
user._conflicts.forEach(function (rev) {
gets.push(self.get(username, {
rev: rev
}).then(function (userRev) {
userRev.roles.forEach(function (role) {
// Build a list of all roles, using an associative array so that duplicates are ignored.
roles[role] = true;
});
}));
});
return Promise.all(gets).then(function () {
// Update the user and set all the roles
user.roles = sporks.keys(roles);
return self._update(username, user);
}).then(function () {
// Delete all the conflicts
user._conflicts.forEach(function (rev) {
destroys.push(self._destroy(username, rev));
});
return Promise.all(destroys);
});
}
});
};
module.exports = User;

View file

@ -11,7 +11,8 @@ describe('user', function () {
user = null,
defaultUpdate = null,
username = null,
defaultRequest = null;
defaultRequest = null,
dbs = [];
beforeEach(function () {
slouch = new Slouch(utils.couchDBURL());
@ -24,9 +25,21 @@ describe('user', function () {
});
});
var destroyDBs = function () {
if (dbs.length > 0) {
var promises = [];
dbs.forEach(function (db) {
promises.push(slouch.db.destroy(db));
});
return Promise.all(promises);
}
};
afterEach(function () {
user._request.request = defaultRequest;
return user.destroy(username);
return user.destroy(username).then(function () {
return destroyDBs();
});
});
var fakeConflict = function () {
@ -181,4 +194,57 @@ describe('user', function () {
}, err);
});
var generateUserConflict = function () {
// Generate a conflict in a user doc by replicating to and from another DB which we will name
// the same name as the username.
return slouch.db.create(username).then(function () {
// Add DB to list that will be destroyed
dbs.push(username);
// Replicate the user
return slouch.db.replicate({
source: slouch._url + '/_users',
target: slouch._url + '/' + username
});
}).then(function () {
// Add a role to the _users docs
return user.addRole(username, 'testrole2');
}).then(function () {
// Add a role to the other DB's doc
return slouch.doc.get(username, user.toUserId(username)).then(function (doc) {
doc.roles = ['testrole1', 'testrole3'];
return slouch.doc.update(username, doc);
});
}).then(function () {
// Replicate the docs to generate a conflict
return slouch.db.replicate({
source: slouch._url + '/' + username,
target: slouch._url + '/_users'
});
}).then(function () {
// Make sure that the doc is in conflict
return user.get(username, {
conflicts: true
});
}).then(function (doc) {
(doc._conflicts.length > 0).should.eql(true);
});
};
it('should resolve conflicts', function () {
return generateUserConflict().then(function () {
return user.resolveConflicts(username);
}).then(function () {
return user.get(username, {
conflicts: true
});
}).then(function (doc) {
// Make sure there no conflicts
(doc._conflicts === undefined).should.eql(true);
// Make sure roles were merged
doc.roles.should.eql(['testrole1', 'testrole2', 'testrole3']);
});
});
});