From ddaa14bb0e5e53e940dadd216aa4b65e2f5dee02 Mon Sep 17 00:00:00 2001 From: Geoff Cox Date: Tue, 5 Sep 2017 14:15:49 -0700 Subject: [PATCH] feat(user): resolve conflicts --- scripts/doc.js | 5 ++-- scripts/user.js | 51 ++++++++++++++++++++++++++++++++-- test/spec/user.js | 70 +++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 119 insertions(+), 7 deletions(-) diff --git a/scripts/doc.js b/scripts/doc.js index 7f3542a..a1c9631 100644 --- a/scripts/doc.js +++ b/scripts/doc.js @@ -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); }; diff --git a/scripts/user.js b/scripts/user.js index 7e29b69..70c1377 100644 --- a/scripts/user.js +++ b/scripts/user.js @@ -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; diff --git a/test/spec/user.js b/test/spec/user.js index 00dfa3a..38a2147 100644 --- a/test/spec/user.js +++ b/test/spec/user.js @@ -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']); + }); + }); + });