From dd594916a06aa608ea4ec33ccba1e8f8096b5d18 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 18 May 2017 00:16:16 +0800 Subject: [PATCH] (#5832) - new feature: force update. * (#5832) - new feature: force update. add a `force` option to `put` operation to allow update based on non-leaf revison and generate a new conflict revision, instead of reporting update conflict error. it is implemented by generating `new_edits=false` option. * optimized force update feature with method proposed by @ronag. need not to fetch `revisions` from db, 2 revisions are enough, namely the new generated one and the old one provided by the user. * bugfix:explictly set radix to 10 for parseInt(). * fix an unintented change caused coverage becomes < 100%. * replace const with var to fix PhantomJS tests failure. * add test case of force putting on second level parent node. * a simple note on the new `force` option for put() api. --- docs/_includes/api/create_document.html | 2 + .../node_modules/pouchdb-core/src/adapter.js | 35 ++++++++- tests/integration/test.conflicts.js | 72 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/docs/_includes/api/create_document.html b/docs/_includes/api/create_document.html index c01fea865a..227c8a7a04 100644 --- a/docs/_includes/api/create_document.html +++ b/docs/_includes/api/create_document.html @@ -7,6 +7,8 @@ Create a new document or update an existing document. If the document already exists, you must specify its revision `_rev`, otherwise a conflict will occur. +If you want to update an existing document even if there's conflict, you should specify the base revision `_rev` and use `force=true` option, then a new conflict revision will be created. + There are some [restrictions on valid property names of the documents](http://wiki.apache.org/couchdb/HTTP_Document_API#Special_Fields). If you try to store non-JSON data (for instance `Date` objects) you may see [inconsistent results](http://pouchdb.com/errors.html#could_not_be_cloned). #### Example Usage: diff --git a/packages/node_modules/pouchdb-core/src/adapter.js b/packages/node_modules/pouchdb-core/src/adapter.js index 22048dc291..9c4b0dff4c 100644 --- a/packages/node_modules/pouchdb-core/src/adapter.js +++ b/packages/node_modules/pouchdb-core/src/adapter.js @@ -1,4 +1,5 @@ import { + uuid, guardedConsole, isRemote } from 'pouchdb-utils'; @@ -215,10 +216,38 @@ AbstractPouchDB.prototype.put = adapterFun('put', function (doc, opts, cb) { return this._putLocal(doc, cb); } } - if (typeof this._put === 'function' && opts.new_edits !== false) { - this._put(doc, opts, cb); + var self = this; + if (opts.force && doc._rev) { + transformForceOptionToNewEditsOption(); + putDoc(function (err) { + var result = err ? null : {ok: true, id: doc._id, rev: doc._rev}; + cb(err, result); + }); } else { - this.bulkDocs({docs: [doc]}, opts, yankError(cb)); + putDoc(cb); + } + + function transformForceOptionToNewEditsOption() { + var parts = doc._rev.split('-'); + var oldRevId = parts[1]; + var oldRevNum = parseInt(parts[0], 10); + + var newRevNum = oldRevNum + 1; + var newRevId = uuid(32, 16).toLowerCase(); + + doc._revisions = { + start: newRevNum, + ids: [newRevId, oldRevId] + }; + doc._rev = newRevNum + '-' + newRevId; + opts.new_edits = false; + } + function putDoc(next) { + if (typeof self._put === 'function' && opts.new_edits !== false) { + self._put(doc, opts, next); + } else { + self.bulkDocs({docs: [doc]}, opts, yankError(next)); + } } }); diff --git a/tests/integration/test.conflicts.js b/tests/integration/test.conflicts.js index 8a47bd18ce..93e1ea3cc3 100644 --- a/tests/integration/test.conflicts.js +++ b/tests/integration/test.conflicts.js @@ -116,6 +116,78 @@ adapters.forEach(function (adapter) { }); }); + it('force put ok on 1st level', function () { + var db = new PouchDB(dbs.name); + var docId = "docId"; + var rev1, rev2, rev3, rev2_; + // given + return db.put({_id: docId, update:1}).then(function (result) { + rev1 = result.rev; + return db.put({_id: docId, update:2.1, _rev: rev1}); + }).then(function (result) { + rev2 = result.rev; + return db.put({_id: docId, update:3, _rev:rev2}); + }) + // when + .then(function (result) { + rev3 = result.rev; + return db.put({_id: docId, update:2.2, _rev: rev1}, {force: true}); + }) + // then + .then(function (result) { + rev2_ = result.rev; + rev2_.should.not.equal(rev3); + rev2_.substring(0, 2).should.equal('2-'); + should.exist(result.ok, 'update based on nonleaf revision'); + + return db.get(docId, {conflicts: true, revs: true}); + }).then(function (doc) { + doc._rev.should.equal(rev3); + doc._conflicts.should.eql([rev2_]); + + return db.get(docId, {conflicts: true, revs: true, rev: rev2_}); + }).then(function (doc) { + console.log("the force doc", doc); + }); + }); + + it('force put ok on 2nd level', function () { + var db = new PouchDB(dbs.name); + var docId = "docId"; + var rev2, rev3, rev4, rev3_; + // given + return db.put({_id: docId, update: 1}).then(function (result) { + return db.put({_id: docId, update: 2, _rev: result.rev}); + }).then(function (result) { + rev2 = result.rev; + return db.put({_id: docId, update: 3.1, _rev: rev2}); + }).then(function (result) { + rev3 = result.rev; + return db.put({_id: docId, update: 4, _rev: rev3}); + }) + // when + .then(function (result) { + rev4 = result.rev; + return db.put({_id: docId, update:3.2, _rev: rev2}, {force: true}); + }) + // then + .then(function (result) { + rev3_ = result.rev; + rev3_.should.not.equal(rev4); + rev3_.substring(0, 2).should.equal('3-'); + should.exist(result.ok, 'update based on nonleaf revision'); + + return db.get(docId, {conflicts: true, revs: true}); + }).then(function (doc) { + doc._rev.should.equal(rev4); + doc._conflicts.should.eql([rev3_]); + + return db.get(docId, {conflicts: true, revs: true, rev: rev3_}); + }).then(function (doc) { + console.log("the force put doc", doc); + }); + }); + // Each revision includes a list of previous revisions. The // revision with the longest revision history list becomes the // winning revision. If they are the same, the _rev values are