From fa7a5f0747b525cbee68c4ac53178720b1273111 Mon Sep 17 00:00:00 2001 From: Tristan Cavelier <tristan.cavelier@tiolive.com> Date: Wed, 16 Jan 2013 12:59:45 +0100 Subject: [PATCH] replicaterevisionstorage.js post command added + tests --- src/jio.storage/replicaterevisionstorage.js | 210 ++++++++++++++++++++ test/jiotests.js | 194 ++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/jio.storage/replicaterevisionstorage.js diff --git a/src/jio.storage/replicaterevisionstorage.js b/src/jio.storage/replicaterevisionstorage.js new file mode 100644 index 0000000..ba981e7 --- /dev/null +++ b/src/jio.storage/replicaterevisionstorage.js @@ -0,0 +1,210 @@ +/*jslint indent: 2, maxlen: 80, nomen: true */ +/*global jIO: true */ +/** + * JIO Replicate Revision Storage. + * It manages storages that manage revisions and conflicts. + * Description: + * { + * "type": "replicaterevision", + * "storage_list": [ + * <sub storage description>, + * ... + * ] + * } + */ +jIO.addStorageType('replicaterevision', function (spec, my) { + "use strict"; + var that, priv = {}; + spec = spec || {}; + that = my.basicStorage(spec, my); + + priv.storage_list_key = "storage_list"; + priv.storage_list = spec[priv.storage_list_key]; + my.env = my.env || spec.env || {}; + + that.specToStore = function () { + var o = {}; + o[priv.storage_list_key] = priv.storage_list; + o.env = my.env; + return o; + }; + + /** + * Generate a new uuid + * @method generateUuid + * @return {string} The new uuid + */ + priv.generateUuid = function () { + var S4 = function () { + var i, string = Math.floor( + Math.random() * 0x10000 /* 65536 */ + ).toString(16); + for (i = string.length; i < 4; i += 1) { + string = "0" + string; + } + return string; + }; + return S4() + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + "-" + + S4() + S4() + S4(); + }; + + /** + * Generates a hash code of a string + * @method hashCode + * @return {string} The next revision + */ + priv.getNextRevision = function (docid) { + my.env[docid].id += 1; + return my.env[docid].id.toString(); + }; + + /** + * Checks a revision format + * @method checkRevisionFormat + * @param {string} revision The revision string + * @return {boolean} True if ok, else false + */ + priv.checkRevisionFormat = function (revision) { + return (/^[0-9a-zA-Z_]+$/.test(revision)); + }; + + /** + * Initalize document environment object + * @method initEnv + * @param {string} docid The document id + * @return {object} The reference to the environment + */ + priv.initEnv = function (docid) { + my.env[docid] = { + "id": 0, + "distant_revisions": {}, + "my_revisions": {}, + "last_revisions": [] + }; + return my.env[docid]; + }; + + /** + * Post the document metadata to all sub storages + * @method post + * @param {object} command The JIO command + */ + that.post = function (command) { + var functions = {}, doc_env, revs_info, doc, my_rev; + functions.begin = function () { + doc = command.cloneDoc(); + + if (typeof doc._rev === "string" && !priv.checkRevisionFormat(doc._rev)) { + that.error({ + "status": 31, + "statusText": "Wrong Revision Format", + "error": "wrong_revision_format", + "message": "The document previous revision does not match " + + "^[0-9]+-[0-9a-zA-Z]+$", + "reason": "Previous revision is wrong" + }); + return; + } + if (typeof doc._id !== "string") { + doc._id = priv.generateUuid(); + } + if (priv.update_doctree_allowed === undefined) { + priv.update_doctree_allowed = true; + } + doc_env = my.env[doc._id]; + if (doc_env && doc_env.id) { + if (!priv.update_doctree_allowed) { + that.error({ + "status": 409, + "statusText": "Conflict", + "error": "conflict", + "message": "Cannot update a document", + "reason": "Document update conflict" + }); + return; + } + } else { + doc_env = priv.initEnv(doc._id); + } + my_rev = priv.getNextRevision(doc._id); + functions.sendDocument(); + }; + functions.sendDocumentIndex = function (method, index, callback) { + var wrapped_callback_success, wrapped_callback_error; + wrapped_callback_success = function (response) { + callback(method, index, undefined, response); + }; + wrapped_callback_error = function (err) { + callback(method, index, err, undefined); + }; + if (typeof doc._rev === "string" && + doc_env.my_revisions[doc._rev] !== undefined) { + doc._rev = doc_env.my_revisions[doc._rev][index]; + } + that.addJob( + method, + priv.storage_list[index], + doc, + command.cloneOption(), + wrapped_callback_success, + wrapped_callback_error + ); + }; + functions.sendDocument = function () { + var i; + doc_env.my_revisions[my_rev] = doc_env.my_revisions[my_rev] || []; + doc_env.my_revisions[my_rev].length = priv.storage_list.length; + for (i = 0; i < priv.storage_list.length; i += 1) { + functions.sendDocumentIndex( + doc_env.last_revisions[i] === "unique_" + i ? "put" : "post", + i, + functions.checkSendResult + ); + } + }; + functions.checkSendResult = function (method, index, err, response) { + if (err) { + if (err.status === 409) { + if (method !== "put") { + functions.sendDocumentIndex( + "put", + index, + functions.checkSendResult + ); + return; + } + } + functions.updateEnv(index, undefined); + functions.error(err); + return; + } + // success + functions.updateEnv(index, response.rev || "unique_" + index); + functions.success({"ok": true, "id": doc._id, "rev": my_rev}); + }; + functions.updateEnv = function (index, revision) { + doc_env.last_revisions[index] = revision; + doc_env.my_revisions[my_rev][index] = revision; + doc_env.distant_revisions[revision] = my_rev; + }; + functions.success = function (response) { + if (!functions.success_called_once) { + functions.success_called_once = true; + that.success(response); + } + }; + functions.error_count = 0; + functions.error = function (err) { + functions.error_count += 1; + if (functions.error_count === priv.storage_list.length) { + that.error(err); + } + }; + functions.begin(); + }; + + return that; +}); diff --git a/test/jiotests.js b/test/jiotests.js index bedc30b..309fb03 100644 --- a/test/jiotests.js +++ b/test/jiotests.js @@ -1813,6 +1813,200 @@ test ("Scenario", function(){ }); + module ("JIO Replicate Revision Storage"); + + var testReplicateRevisionStorageGenerator = function ( + sinon, jio_description, document_name_have_revision + ) { + + var o = generateTools(sinon), leavesAction, generateLocalPath; + + o.jio = JIO.newJio(jio_description); + + generateLocalPath = function (storage_description) { + return "jio/localstorage/" + storage_description.username + "/" + + storage_description.application_name; + }; + + leavesAction = function (action, storage_description, param) { + var i; + if (param === undefined) { + param = {}; + } else { + param = clone(param); + } + if (storage_description.storage_list !== undefined) { + // it is the replicate revision storage tree + for (i = 0; i < storage_description.storage_list.length; i += 1) { + leavesAction(action, storage_description.storage_list[i], param); + } + } else if (storage_description.sub_storage !== undefined) { + // it is the revision storage tree + param.revision = true; + leavesAction(action, storage_description.sub_storage, param); + } else { + // it is the storage tree leaf + param[storage_description.type] = true; + action(storage_description, param); + } + }; + o.leavesAction = function (action) { + leavesAction(action, jio_description); + }; + + // post a new document without id + o.doc = {"title": "post document without id"}; + o.revision = {"start": 0, "ids": []}; + o.spy(o, "status", undefined, "Post document (without id)"); + o.jio.post(o.doc, function (err, response) { + o.f.apply(arguments); + o.response_rev = (err || response).rev; + if (isUuid((err || response).id)) { + ok(true, "Uuid format"); + o.uuid = (err || response).id; + } else { + deepEqual((err || response).id, + "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "Uuid format"); + } + }); + o.tick(o); + + // check document + o.doc._id = o.uuid; + o.rev = "1"; + o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision); + o.leavesAction(function (storage_description, param) { + var suffix = "", doc = clone(o.doc); + if (param.revision) { + deepEqual(o.response_rev, o.rev, "Check revision"); + doc._id += "." + o.local_rev; + suffix = "." + o.local_rev; + } + deepEqual( + localstorage.getItem(generateLocalPath(storage_description) + + "/" + o.uuid + suffix), + doc, "Check document" + ); + }); + + // post a new document with id + o.doc = {"_id": "post1", "title": "post new doc with id"}; + o.rev = "1" + o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev}, + "Post document (with id)"); + o.jio.post(o.doc, o.f); + o.tick(o); + + // check document + o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision); + o.leavesAction(function (storage_description, param) { + var suffix = "", doc = clone(o.doc); + if (param.revision) { + doc._id += "." + o.local_rev; + suffix = "." + o.local_rev; + } + deepEqual( + localstorage.getItem(generateLocalPath(storage_description) + + "/post1" + suffix), + doc, "Check document" + ); + }); + + // post same document without revision + o.doc = {"_id": "post1", "title": "post same document without revision"}; + o.rev = "2"; + o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev}, + "Post same document (without revision)"); + o.jio.post(o.doc, o.f); + o.tick(o); + + // check document + o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision); + o.leavesAction(function (storage_description, param) { + var suffix = "", doc = clone(o.doc); + if (param.revision) { + doc._id += "." + o.local_rev; + suffix = "." + o.local_rev; + } + deepEqual( + localstorage.getItem(generateLocalPath(storage_description) + + "/post1" + suffix), + doc, "Check document" + ); + }); + + // post a new revision + o.doc = {"_id": "post1", "title": "post new revision", "_rev": o.rev}; + o.rev = "3"; + o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev}, + "Post document (with revision)"); + o.jio.post(o.doc, o.f); + o.tick(o); + + // check document + o.revision.start += 1; + o.revision.ids.unshift(o.local_rev.split("-").slice(1).join("-")); + o.doc._rev = o.local_rev; + o.local_rev = "2-" + generateRevisionHash(o.doc, o.revision); + o.leavesAction(function (storage_description, param) { + var suffix = "", doc = clone(o.doc); + delete doc._rev; + if (param.revision) { + doc._id += "." + o.local_rev; + suffix = "." + o.local_rev; + } + deepEqual( + localstorage.getItem(generateLocalPath(storage_description) + + "/post1" + suffix), + doc, "Check document" + ); + }); + + o.jio.stop(); + + }; + + test ("[Local Storage] Scenario", function () { + testReplicateRevisionStorageGenerator(this, { + "type": "replicaterevision", + "storage_list": [{ + "type": "local", + "username": "ureploc", + "application_name": "areploc" + }] + }); + }); + test ("[Revision + Local Storage] Scenario", function () { + testReplicateRevisionStorageGenerator(this, { + "type": "replicaterevision", + "storage_list": [{ + "type": "revision", + "sub_storage": { + "type": "local", + "username": "ureprevloc", + "application_name": "areprevloc" + } + }] + }); + }); + test ("[Revision + Local Storage, Local Storage] Scenario", function () { + testReplicateRevisionStorageGenerator(this, { + "type": "replicaterevision", + "storage_list": [{ + "type": "revision", + "sub_storage": { + "type": "local", + "username": "ureprevlocloc", + "application_name": "areprevlocloc" + } + },{ + "type": "local", + "username": "ureprevlocloc2", + "application_name": "areprevlocloc2" + }] + }); + }); + /* module ('Jio DAVStorage'); -- 2.30.9