Commit d7fcbbaf authored by Tristan Cavelier's avatar Tristan Cavelier

Revision storage redesigned to avoid some unknown errors

parent 049ea125
......@@ -187,6 +187,253 @@ jIO.addStorageType('replicaterevision', function (spec, my) {
}
};
////////////////////////////////////////////////////////////////////////////////
that.check = function (command) {
priv.check(
command.cloneDoc(),
command.cloneOption(),
that.success,
that.error
);
};
that.repair = function (command) {
priv.repair(
command.cloneDoc(),
command.cloneOption(),
true,
that.success,
that.error
);
};
priv.check = function (doc, option, success, error) {
priv.repair(doc, option, false, success, error);
};
priv.repair = function (doc, option, repair, callback) {
var functions = {};
// console.log("priv.repair");
callback = callback || priv.emptyFunction;
option = option || {};
functions.begin = function () {
// console.log("repair begin");
functions.getAllDocuments(functions.newParam(
doc,
option,
repair
));
};
functions.newParam = function (doc, option, repair) {
// console.log("repair new param");
var param = {
"doc": doc,
"option": option,
"repair": repair,
"responses": {
"count": 0,
"list": [
// 0: response0
// 1: response1
// 2: response2
],
"stats": {
// responseA: [0, 1]
// responseB: [2]
},
"stats_items": [
// 0: [responseA, [0, 1]]
// 1: [responseB, [2]]
]
},
"conflicts": {
// revC: true
// revD: true
},
"deal_result_state": "ok",
"my_rev": undefined
};
param.responses.list.length = priv.storage_list.length;
return param;
};
functions.getAllDocuments = function (param) {
// console.log("repair getAllDocument");
var i, doc = priv.clone(param.doc), option = priv.clone(param.option);
option.conflicts = true;
option.revs = true;
option.revs_info = true;
for (i = 0; i < priv.storage_list.length; i += 1) {
// if the document is not loaded
priv.send("get", i, doc, option, functions.dealResults(param));
}
functions.finished_count += 1;
};
functions.dealResults = function (param) {
// console.log("repair dealResults");
return function (method, index, err, response) {
if (param.deal_result_state !== "ok") {
// deal result is in a wrong state, exit
// console.log("repair dealResults wrong state");
return;
}
if (err) {
if (err.status !== 404) {
// get document failed, exit
param.deal_result_state = "error";
// console.log("repair dealResults error");
callback({
"status": 40,
"statusText": "Check Failed",
"error": "check_failed",
"message": "An error occured on the sub storage",
"reason": err.reason
});
return;
}
}
// success to get the document
// add the response in memory
param.responses.count += 1;
param.responses.list[index] = response;
// add the conflicting revision for other synchronizations
functions.addConflicts(param, (response || {})._conflicts);
if (param.responses.count !== param.responses.list.length) {
// this is not the last response, wait for the next response
// console.log("repair dealResults not last");
return;
}
// console.log("repair dealResults last");
// this is now the last response
functions.makeResponsesStats(param.responses);
if (param.responses.stats_items.length === 1) {
// the responses are equals!
// console.log("repair dealResults OK");
callback(undefined, {
"ok": true,
"id": param.doc._id,
"rev": (typeof param.responses.list[0] === "object" ?
param.responses.list[0]._rev : undefined)
});
return;
}
// the responses are different
if (param.repair === false) {
// do not repair
callback({
"status": 41,
"statusText": "Check Not Ok",
"error": "check_not_ok",
"message": "Some documents are different in the sub storages",
"reason": "Storage contents differ"
});
return;
}
// repair
functions.synchronizeAllSubStorage(param);
if (param.option.synchronize_conflicts !== false) {
functions.synchronizeConflicts(param);
}
};
};
functions.addConflicts = function (param, list) {
// console.log("repair addConflicts");
var i;
list = list || [];
for (i = 0; i < list.length; i += 1) {
param.conflicts[list[i]] = true;
}
};
functions.makeResponsesStats = function (responses) {
// console.log("repair makeResponseStats");
var i, str_response;
for (i = 0; i < responses.count; i += 1) {
str_response = JSON.stringify(responses.list[i]);
if (responses.stats[str_response] === undefined) {
responses.stats[str_response] = [];
responses.stats_items.push([
str_response,
responses.stats[str_response]
]);
}
responses.stats[str_response].push(i);
}
};
functions.synchronizeAllSubStorage = function (param) {
// console.log("repair synchronizeAllSubStorage");
var i, j, len = param.responses.stats_items.length;
for (i = 0; i < len; i += 1) {
// browsing responses
for (j = 0; j < len; j += 1) {
// browsing storage list
if (i !== j) {
functions.synchronizeResponseToSubStorage(
param,
param.responses.stats_items[i][0],
param.responses.stats_items[j][1]
);
}
}
}
functions.finished_count -= 1;
};
functions.synchronizeResponseToSubStorage = function (
param,
response,
storage_list
) {
// console.log("repair synchronizeResponseToSubStorage");
var i, new_doc;
if (response === undefined) {
// no response to sync
return;
}
for (i = 0; i < storage_list.length; i += 1) {
new_doc = JSON.parse(response);
new_doc._revs = new_doc._revisions;
delete new_doc._rev;
delete new_doc._revisions;
delete new_doc._conflicts;
functions.finished_count += 1;
priv.send(
"put",
storage_list[i],
new_doc,
param.option,
functions.finished
);
}
};
functions.synchronizeConflicts = function (param) {
// console.log("repair synchronizeConflicts");
var rev, new_doc, new_option;
new_option = priv.clone(param.option);
new_option.synchronize_conflict = false;
for (rev in param.conflicts) {
if (param.conflicts.hasOwnProperty(rev)) {
new_doc = priv.clone(param.doc);
new_doc._rev = rev;
// no need to synchronize all the conflicts again, do it once
functions.getAllDocuments(functions.newParam(
new_doc,
new_option,
param.repair
));
}
}
};
functions.finished_count = 0;
functions.finished = function () {
// console.log("repair finished " + functions.finished_count);
functions.finished_count -= 1;
if (functions.finished_count === 0) {
// console.log("repair ended");
callback(undefined, {"ok": true, "id": doc._id});
}
};
functions.begin();
};
////////////////////////////////////////////////////////////////////////////////
/**
* Post the document metadata to all sub storages
* @method post
......@@ -397,6 +644,9 @@ jIO.addStorageType('replicaterevision', function (spec, my) {
}
}
that.success(response);
setTimeout(function () {
priv.repair({"_id": doc._id}, command.cloneOption(), true);
});
};
functions.error_count = 0;
functions.error = function (err) {
......
......@@ -9,20 +9,31 @@
* "sub_storage": <sub storage description>
* }
*/
jIO.addStorageType('revision', function (spec, my) {
jIO.addStorageType("revision", function (spec, my) {
"use strict";
var that, priv = {};
var that = {}, priv = {};
spec = spec || {};
that = my.basicStorage(spec, my);
// ATTRIBUTES //
priv.doc_tree_suffix = ".revision_tree.json";
priv.sub_storage = spec.sub_storage;
// METHODS //
/**
* Constructor
*/
priv.RevisionStorage = function () {
// no init
};
priv.substorage_key = "sub_storage";
priv.doctree_suffix = ".revision_tree.json";
priv.substorage = spec[priv.substorage_key];
/**
* Description to store in order to be restored later
* @method specToStore
* @return {object} Descriptions to store
*/
that.specToStore = function () {
var o = {};
o[priv.substorage_key] = priv.substorage;
return o;
return {
"sub_storage": priv.sub_storage
};
};
/**
......@@ -70,1200 +81,836 @@ jIO.addStorageType('revision', function (spec, my) {
};
/**
* Returns an array version of a revision string
* @method revisionToArray
* @param {string} revision The revision string
* @return {array} Array containing a revision number and a hash
* Checks a revision format
* @method checkDocumentRevisionFormat
* @param {object} doc The document object
* @return {object} null if ok, else error object
*/
priv.revisionToArray = function (revision) {
if (typeof revision === "string") {
return [parseInt(revision.split('-')[0], 10),
revision.split('-')[1]];
priv.checkDocumentRevisionFormat = function (doc) {
var send_error = function (message) {
return {
"status": 31,
"statusText": "Wrong Revision Format",
"error": "wrong_revision_format",
"message": message,
"reason": "Revision is wrong"
};
};
if (typeof doc._rev === "string") {
if (/^[0-9]+-[0-9a-zA-Z]+$/.test(doc._rev) === false) {
return send_error("The document revision does not match " +
"^[0-9]+-[0-9a-zA-Z]+$");
}
}
if (typeof doc._revs === "object") {
if (typeof doc._revs.start !== "number" ||
typeof doc._revs.ids !== "object" ||
typeof doc._revs.ids.length !== "number") {
return send_error("The document revision history is not well formated");
}
}
if (typeof doc._revs_info === "object") {
if (typeof doc._revs_info.length !== "number") {
return send_error("The document revision information " +
"is not well formated");
}
}
return revision;
};
/**
* Convert the revision history object to an array of revisions.
* @method revisionHistoryToArray
* @param {object} revs The revision history
* @return {array} The revision array
* Creates a new document tree
* @method newDocTree
* @return {object} The new document tree
*/
priv.revisionHistoryToArray = function (revs) {
var i, start = revs.start, newlist = [];
for (i = 0; i < revs.ids.length; i += 1, start -= 1) {
newlist.push(start + "-" + revs.ids[i]);
}
return newlist;
priv.newDocTree = function () {
return {"children": []};
};
/**
* Generates the next revision of [previous_revision]. [string] helps us
* to generate a hash code.
* @methode generateNextRev
* @param {string} previous_revision The previous revision
* @param {object} doc The document metadata
* @param {object} revisions The revision history
* @param {boolean} deleted_flag The deleted flag
* @return {array} 0:The next revision number and 1:the hash code
* Convert revs_info to a simple revisions history
* @method revsInfoToHistory
* @param {array} revs_info The revs info
* @return {object} The revisions history
*/
priv.generateNextRevision = function (previous_revision,
doc, revisions, deleted_flag) {
var string = JSON.stringify(doc) + JSON.stringify(revisions) +
JSON.stringify(deleted_flag ? true : false);
if (typeof previous_revision === "number") {
return [previous_revision + 1, priv.hashCode(string)];
priv.revsInfoToHistory = function (revs_info) {
var i, revisions = {
"start": 0,
"ids": []
};
revs_info = revs_info || [];
if (revs_info.length > 0) {
revisions.start = parseInt(revs_info[0].rev.split('-')[0], 10);
}
for (i = 0; i < revs_info.length; i += 1) {
revisions.ids.push(revs_info[i].rev.split('-')[1]);
}
previous_revision = priv.revisionToArray(previous_revision);
return [previous_revision[0] + 1, priv.hashCode(string)];
return revisions;
};
/**
* Checks a revision format
* @method checkRevisionFormat
* @param {string} revision The revision string
* @return {boolean} True if ok, else false
* Convert the revision history object to an array of revisions.
* @method revisionHistoryToList
* @param {object} revs The revision history
* @return {array} The revision array
*/
priv.checkRevisionFormat = function (revision) {
return (/^[0-9]+-[0-9a-zA-Z]+$/.test(revision));
priv.revisionHistoryToList = function (revs) {
var i, start = revs.start, new_list = [];
for (i = 0; i < revs.ids.length; i += 1, start -= 1) {
new_list.push(start + "-" + revs.ids[i]);
}
return new_list;
};
/**
* Creates an empty document tree
* @method createDocumentTree
* @param {array} children An array of children (optional)
* @return {object} The new document tree
* Convert revision list to revs info.
* @method revisionListToRevsInfo
* @param {array} revision_list The revision list
* @param {object} doc_tree The document tree
* @return {array} The document revs info
*/
priv.createDocumentTree = function (children) {
return {
"children": children || []
priv.revisionListToRevsInfo = function (revision_list, doc_tree) {
var revisionListToRevsInfoRec, revs_info = [], j;
for (j = 0; j < revision_list.length; j += 1) {
revs_info.push({"rev": revision_list[j], "status": "missing"});
}
revisionListToRevsInfoRec = function (index, doc_tree) {
var child, i;
if (index < 0) {
return;
}
for (i = 0; i < doc_tree.children.length; i += 1) {
child = doc_tree.children[i];
if (child.rev === revision_list[index]) {
revs_info[index].status = child.status;
revisionListToRevsInfoRec(index - 1, child);
}
}
};
revisionListToRevsInfoRec(revision_list.length - 1, doc_tree);
return revs_info;
};
/**
* Creates a new document tree node
* @method createDocumentTreeNode
* @param {string} revision The node revision
* @param {string} status The node status
* @param {array} children An array of children (optional)
* @return {object} The new document tree node
* Update a document metadata revision properties
* @method fillDocumentRevisionProperties
* @param {object} doc The document object
* @param {object} doc_tree The document tree
*/
priv.createDocumentTreeNode = function (revision, status, children) {
return {
"rev": revision,
"status": status,
"children": children || []
};
priv.fillDocumentRevisionProperties = function (doc, doc_tree) {
if (doc._revs_info) {
doc._revs = priv.revsInfoToHistory(doc._revs_info);
} else if (doc._revs) {
doc._revs_info = priv.revisionListToRevsInfo(
priv.revisionHistoryToList(doc._revs),
doc_tree
);
} else if (doc._rev) {
doc._revs_info = priv.getRevisionInfo(doc._rev, doc_tree);
doc._revs = priv.revsInfoToHistory(doc._revs_info);
} else {
doc._revs_info = [];
doc._revs = {"start": 0, "ids": []};
}
if (doc._revs.start > 0) {
doc._rev = doc._revs.start + "-" + doc._revs.ids[0];
} else {
delete doc._rev;
}
};
/**
* Gets the specific revision from a document tree.
* @method getRevisionFromDocumentTree
* @param {object} document_tree The document tree
* @param {string} revision The specific revision
* @return {array} The good revs info array
* Generates the next revision of a document.
* @methode generateNextRevision
* @param {object} doc The document metadata
* @param {boolean} deleted_flag The deleted flag
* @return {array} 0:The next revision number and 1:the hash code
*/
priv.getRevisionFromDocumentTree = function (document_tree, revision) {
var result, search, revs_info = [];
result = [];
// search method fills "result" with the good revs info
search = function (document_tree) {
var i;
if (document_tree.rev !== undefined) {
// node is not root
revs_info.unshift({
"rev": document_tree.rev,
"status": document_tree.status
});
if (document_tree.rev === revision) {
result = revs_info;
return;
priv.generateNextRevision = function (doc, deleted_flag) {
var string, revision_history, revs_info, pseudo_revision;
doc = priv.clone(doc) || {};
revision_history = doc._revs;
revs_info = doc._revs_info;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
string = JSON.stringify(doc) + JSON.stringify(revision_history) +
JSON.stringify(deleted_flag ? true : false);
console.log(string);
revision_history.start += 1;
revision_history.ids.unshift(priv.hashCode(string));
doc._revs = revision_history;
doc._rev = revision_history.start + "-" + revision_history.ids[0];
revs_info.unshift({
"rev": doc._rev,
"status": deleted_flag ? "deleted" : "available"
});
doc._revs_info = revs_info;
return doc;
};
/**
* Gets the revs info from the document tree
* @method getRevisionInfo
* @param {string} revision The revision to search for
* @param {object} doc_tree The document tree
* @return {array} The revs info
*/
priv.getRevisionInfo = function (revision, doc_tree) {
var getRevisionInfoRec;
getRevisionInfoRec = function (doc_tree) {
var i, child, revs_info;
for (i = 0; i < doc_tree.children.length; i += 1) {
child = doc_tree.children[i];
if (child.rev === revision) {
return [{"rev": child.rev, "status": child.status}];
}
}
// This node has children
for (i = 0; i < document_tree.children.length; i += 1) {
// searching deeper to find the good rev
search(document_tree.children[i]);
if (result.length > 0) {
// The result is already found
return;
revs_info = getRevisionInfoRec(child);
if (revs_info.length > 0 || revision === undefined) {
revs_info.push({"rev": child.rev, "status": child.status});
return revs_info;
}
revs_info.shift();
}
return [];
};
search(document_tree);
return result;
return getRevisionInfoRec(doc_tree);
};
/**
* Gets the winner revision from a document tree.
* The winner is the deeper revision on the left.
* @method getWinnerRevisionFromDocumentTree
* @param {object} document_tree The document tree
* @return {array} The winner revs info array
*/
priv.getWinnerRevisionFromDocumentTree = function (document_tree) {
var result, search, revs_info = [];
result = [];
// search method fills "result" with the winner revs info
search = function (document_tree, deep) {
var i;
if (document_tree.rev !== undefined) {
// node is not root
revs_info.unshift({
"rev": document_tree.rev,
"status": document_tree.status
});
}
if (document_tree.children.length === 0 && document_tree.status !==
"deleted") {
// This node is a leaf
if (result.length < deep) {
// The leaf is deeper than result
result = [];
for (i = 0; i < revs_info.length; i += 1) {
result.push(revs_info[i]);
}
}
priv.updateDocumentTree = function (doc, doc_tree) {
var revs_info, updateDocumentTreeRec, next_rev;
doc = priv.clone(doc);
revs_info = doc._revs_info;
updateDocumentTreeRec = function (doc_tree, revs_info) {
var i, child, info;
if (revs_info.length === 0) {
return;
}
// This node has children
for (i = 0; i < document_tree.children.length; i += 1) {
// searching deeper to find the deeper leaf
search(document_tree.children[i], deep + 1);
revs_info.shift();
info = revs_info.pop();
for (i = 0; i < doc_tree.children.length; i += 1) {
child = doc_tree.children[i];
if (child.rev === info.rev) {
return updateDocumentTreeRec(child, revs_info);
}
}
doc_tree.children.unshift({
"rev": info.rev,
"status": info.status,
"children": []
});
updateDocumentTreeRec(doc_tree.children[0], revs_info);
};
search(document_tree, 0);
return result;
updateDocumentTreeRec(doc_tree, priv.clone(revs_info));
};
/**
* Add a document revision branch to the document tree
* @method updateDocumentTree
* @param {object} doctree The document tree object
* @param {object|array} revs The revision history object or a revision array
* @param {boolean} deleted The deleted flag
* @param {array} The document revs_info
*/
priv.updateDocumentTree = function (doctree, revs, deleted) {
var revs_info, doctree_iterator, flag, i, rev;
revs_info = [];
if (revs.ids) {
// revs is a revision history object
revs = priv.revisionHistoryToArray(revs);
} else {
// revs is an array of revisions
revs = priv.clone(revs);
}
doctree_iterator = doctree;
while (revs.length > 0) {
rev = revs.pop(0);
revs_info.unshift({
"rev": rev,
"status": "missing"
});
for (i = 0; i < doctree_iterator.children.length; i += 1) {
if (doctree_iterator.children[i].rev === rev) {
doctree_iterator = doctree_iterator.children[i];
revs_info[0].status = doctree_iterator.status;
rev = undefined;
break;
}
}
if (rev) {
doctree_iterator.children.unshift({
"rev": rev,
"status": "missing",
"children": []
});
doctree_iterator = doctree_iterator.children[0];
priv.send = function (method, doc, option, callback) {
that.addJob(
method,
priv.sub_storage,
doc,
option,
function (success) {
callback(undefined, success);
},
function (err) {
callback(err, undefined);
}
}
flag = deleted === true ? "deleted" : "available";
revs_info[0].status = flag;
doctree_iterator.status = flag;
return revs_info;
);
};
/**
* Add a document revision to the document tree
* @method postToDocumentTree
* @param {object} doctree The document tree object
* @param {object} doc The document object
* @param {boolean} set_node_to_deleted Set the revision to deleted
* @return {array} The added document revs_info
*/
priv.postToDocumentTree = function (doctree, doc, set_node_to_deleted) {
var i, revs_info, next_rev, next_rev_str, selectNode, selected_node,
flag;
flag = set_node_to_deleted === true ? "deleted" : "available";
revs_info = [];
selected_node = doctree;
selectNode = function (node) {
priv.getWinnerRevsInfo = function (doc_tree) {
var revs_info = [], getWinnerRevsInfoRec;
getWinnerRevsInfoRec = function (doc_tree, tmp_revs_info) {
var i;
if (node.rev !== undefined) {
// node is not root
revs_info.unshift({
"rev": node.rev,
"status": node.status
});
}
if (node.rev === doc._rev) {
selected_node = node;
return "node_selected";
if (doc_tree.rev) {
tmp_revs_info.unshift({"rev": doc_tree.rev, "status": doc_tree.status});
}
for (i = 0; i < node.children.length; i += 1) {
if (selectNode(node.children[i]) === "node_selected") {
return "node_selected";
if (doc_tree.children.length === 0) {
if (revs_info.length < tmp_revs_info.length ||
(revs_info.length > 0 && revs_info[0].status === "deleted")) {
revs_info = priv.clone(tmp_revs_info);
}
revs_info.shift();
}
};
if (typeof doc._rev === "string") {
// document has a previous revision
if (selectNode(selected_node) !== "node_selected") {
// no node was selected, so add a node with a specific rev
revs_info.unshift({
"rev": doc._rev,
"status": "missing"
});
selected_node.children.unshift(priv.createDocumentTreeNode(
doc._rev,
"missing"
));
selected_node = selected_node.children[0];
}
}
next_rev = priv.generateNextRevision(
doc._rev || 0,
doc,
priv.revsInfoToHistory(revs_info),
set_node_to_deleted
);
next_rev_str = next_rev.join("-");
// don't add if the next rev already exists
for (i = 0; i < selected_node.children.length; i += 1) {
if (selected_node.children[i].rev === next_rev_str) {
revs_info.unshift({
"rev": next_rev_str,
"status": flag
});
if (selected_node.children[i].status !== flag) {
selected_node.children[i].status = flag;
}
return revs_info;
for (i = 0; i < doc_tree.children.length; i += 1) {
getWinnerRevsInfoRec(doc_tree.children[i], tmp_revs_info);
}
}
revs_info.unshift({
"rev": next_rev.join('-'),
"status": flag
});
selected_node.children.unshift(priv.createDocumentTreeNode(
next_rev.join('-'),
flag
));
tmp_revs_info.shift();
};
getWinnerRevsInfoRec(doc_tree, []);
return revs_info;
};
/**
* Gets an array of leaves revisions from document tree
* @method getLeavesFromDocumentTree
* @param {object} document_tree The document tree
* @param {string} except The revision to except
* @return {array} The array of leaves revisions
*/
priv.getLeavesFromDocumentTree = function (document_tree, except) {
var result, search;
result = [];
// search method fills [result] with the winner revision
search = function (document_tree) {
priv.getConflicts = function (revision, doc_tree) {
var conflicts = [], getConflictsRec;
getConflictsRec = function (doc_tree) {
var i;
if (except !== undefined && except === document_tree.rev) {
if (doc_tree.rev === revision) {
return;
}
if (document_tree.children.length === 0 && document_tree.status !==
"deleted") {
// This node is a leaf
result.push(document_tree.rev);
return;
if (doc_tree.children.length === 0) {
if (doc_tree.status !== "deleted") {
conflicts.push(doc_tree.rev);
}
}
// This node has children
for (i = 0; i < document_tree.children.length; i += 1) {
// searching deeper to find the deeper leaf
search(document_tree.children[i]);
for (i = 0; i < doc_tree.children.length; i += 1) {
getConflictsRec(doc_tree.children[i]);
}
};
search(document_tree);
return result;
getConflictsRec(doc_tree);
return conflicts.length === 0 ? undefined : conflicts;
};
/**
* Check if revision is a leaf
* @method isRevisionALeaf
* @param {string} revision revision to check
* @param {array} leaves all leaves on tree
* @return {boolean} true/false
*/
priv.isRevisionALeaf = function (document_tree, revision) {
var result, search;
result = undefined;
// search method fills "result" with the good revs info
search = function (document_tree) {
var i;
if (document_tree.rev !== undefined) {
// node is not root
if (document_tree.rev === revision) {
if (document_tree.children.length === 0) {
// This node is a leaf
result = true;
return;
}
result = false;
priv.get = function (doc, option, callback) {
priv.send("get", doc, option, callback);
};
priv.put = function (doc, option, callback) {
console.log(doc);
priv.send("put", doc, option, callback);
};
priv.remove = function (doc, option, callback) {
priv.send("remove", doc, option, callback);
};
priv.putAttachment = function (attachment, option, callback) {
priv.send("putAttachment", attachment, option, callback);
};
priv.getDocument = function (doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + "." + doc._rev;
delete doc._attachment;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
priv.get(doc, option, callback);
};
priv.getAttachment = priv.get;
priv.putDocument = function (doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + "." + doc._rev;
delete doc._attachment;
delete doc._data;
delete doc._mimetype;
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
priv.put(doc, option, callback);
};
priv.getRevisionTree = function (doc, option, callback) {
doc = priv.clone(doc);
doc._id = doc._id + priv.doc_tree_suffix;
priv.get(doc, option, callback);
};
priv.getAttachmentList = function (doc, option, callback) {
console.log("p getAttachmentList");
var attachment_id, dealResults, state = "ok", result_list = [], count = 0;
dealResults = function (attachment_id, attachment_meta) {
return function (err, attachment) {
if (state !== "ok") {
return;
}
}
// This node has children
for (i = 0; i < document_tree.children.length; i += 1) {
// searching deeper to find the good rev
search(document_tree.children[i]);
if (result !== undefined) {
// The result is already found
return;
count -= 1;
if (err) {
if (err.status === 404) {
result_list.push(undefined);
} else {
state = "error";
return callback(err, undefined);
}
}
}
result_list.push({
"_attachment": attachment_id,
"_data": attachment,
"_mimetype": attachment_meta.content_type
});
if (count === 0) {
state = "finished";
callback(undefined, result_list);
}
};
};
search(document_tree);
return result || false;
};
/**
* Convert revs_info to a simple revisions history
* @method revsInfoToHistory
* @param {array} revs_info The revs info
* @return {object} The revisions history
*/
priv.revsInfoToHistory = function (revs_info) {
var revisions = {
"start": 0,
"ids": []
}, i;
if (revs_info.length > 0) {
revisions.start = parseInt(revs_info[0].rev.split('-')[0], 10);
for (attachment_id in doc._attachments) {
if (doc._attachments.hasOwnProperty(attachment_id)) {
count += 1;
priv.get(
{"_id": doc._id + "/" + attachment_id},
option,
dealResults(attachment_id, doc._attachments[attachment_id])
);
}
}
for (i = 0; i < revs_info.length; i += 1) {
revisions.ids.push(revs_info[i].rev.split('-')[1]);
if (count === 0) {
callback(undefined, []);
}
return revisions;
};
/**
* Returns the revision of the revision position from a revs_info array.
* @method getRevisionFromPosition
* @param {array} revs_info The revs_info array
* @param {number} rev_pos The revision position number
* @return {string} The revision of the good position (empty string if fail)
*/
priv.getRevisionFromPosition = function (revs_info, rev_pos) {
var i;
for (i = revs_info.length - 1; i >= 0; i -= 1) {
if (priv.revisionToArray(revs_info[i].rev)[0] === rev_pos) {
return revs_info[i].rev;
priv.putAttachmentList = function (doc, option, attachment_list, callback) {
console.log("p putAttachmentList");
var i, dealResults, state = "ok", count = 0, attachment;
attachment_list = attachment_list || [];
dealResults = function (index) {
return function (err, response) {
if (state !== "ok") {
return;
}
count -= 1;
if (err) {
state = "error";
return callback(err, undefined);
}
if (count === 0) {
state = "finished";
callback(undefined, {"id": doc._id, "ok": true});
}
};
};
for (i = 0; i < attachment_list.length; i += 1) {
attachment = attachment_list[i];
if (attachment !== undefined) {
count += 1;
attachment._id = doc._id + "." + doc._rev + "/" +
attachment._attachment;
delete attachment._attachment;
priv.putAttachment(attachment, option, dealResults(i));
}
}
return '';
if (count === 0) {
return callback(undefined, {"id": doc._id, "ok": true});
}
};
/**
* Post the document metadata and create or update a document tree.
* Options:
* - {boolean} keep_revision_history To keep the previous revisions
* (false by default) (NYI).
* @method post
* @param {object} command The JIO command
*/
that.post = function (command) {
var f = {}, doctree, revs_info, doc, docid, prev_doc;
doc = command.cloneDoc();
docid = command.getDocId();
priv.putDocumentTree = function (doc, option, doc_tree, callback) {
doc_tree = priv.clone(doc_tree);
doc_tree._id = doc._id + priv.doc_tree_suffix;
priv.put(doc_tree, option, callback);
};
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;
priv.notFoundError = function (message, reason) {
return {
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": message,
"reason": reason
};
};
priv.conflictError = function (message, reason) {
return {
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": message,
"reason": reason
};
};
priv.revisionGenericRequest = function (doc, option,
specific_parameter, onEnd) {
var prev_doc, doc_tree, attachment_list, callback = {};
if (specific_parameter.doc_id) {
doc._id = specific_parameter.doc_id;
}
if (typeof docid !== "string") {
doc._id = priv.generateUuid();
docid = doc._id;
if (specific_parameter.attachment_id) {
doc._attachment = specific_parameter.attachment_id;
}
f.getDocumentTree = function () {
var option = command.cloneOption();
if (option.max_retry === 0) {
option.max_retry = 3;
callback.begin = function () {
console.log("c begin");
var check_error;
doc._id = doc._id || priv.generateUuid();
if (specific_parameter.revision_needed && !doc._rev) {
return onEnd(priv.conflictError(
"Document update conflict",
"No document revision was provided"
), undefined);
}
// check revision format
check_error = priv.checkDocumentRevisionFormat(doc);
if (check_error !== undefined) {
return onEnd(check_error, undefined);
}
priv.getRevisionTree(doc, option, callback.getRevisionTree);
};
callback.getRevisionTree = function (err, response) {
console.log("c getRevisionTree");
var winner_info, previous_revision = doc._rev,
generate_new_revision = doc._revs || doc._revs_info ? false : true;
if (err) {
if (err.status !== 404) {
err.message = "Cannot get document revision tree";
return onEnd(err, undefined);
}
}
that.addJob(
"get",
priv.substorage,
docid + priv.doctree_suffix,
option,
function (response) {
doctree = response;
f.updateRevsInfo();
f.getDocument();
},
function (err) {
switch (err.status) {
case 404:
doctree = priv.createDocumentTree();
f.updateRevsInfo();
f.getDocument();
break;
default:
err.message = "Cannot get document revision tree";
f.error(err);
break;
doc_tree = response || priv.newDocTree();
if (specific_parameter.get || specific_parameter.getAttachment) {
if (!doc._rev) {
console.log(JSON.stringify(doc_tree));
winner_info = priv.getWinnerRevsInfo(doc_tree);
console.log(winner_info);
if (winner_info.length === 0) {
return onEnd(priv.notFoundError(
"Document not found",
"missing"
), undefined);
}
}
);
};
f.getDocument = function () {
if (revs_info[1] === undefined) {
f.postDocument([]);
} else {
that.addJob(
"get",
priv.substorage,
command.getDocId() + "." + revs_info[1].rev,
command.getOption(),
function (response) {
var attachment_list = [], i;
prev_doc = response;
for (i in response._attachments) {
if (response._attachments.hasOwnProperty(i)) {
attachment_list.push({"id": i, "attachment": {
"_id": command.getDocId() + "." + revs_info[0].rev + "/" + i,
"_mimetype": response._attachments[i].content_type,
"_data": undefined
}});
}
}
f.postDocument(attachment_list);
},
function (err) {
if (err.status === 404) {
f.postDocument([]);
return;
}
err.message = "Cannot retrieve document";
f.error(err);
if (winner_info[0].status === "deleted") {
return onEnd(priv.notFoundError(
"Document not found",
"deleted"
), undefined);
}
doc._rev = winner_info[0].rev;
}
priv.fillDocumentRevisionProperties(doc, doc_tree);
return priv.getDocument(doc, option, callback.getDocument);
}
priv.fillDocumentRevisionProperties(doc, doc_tree);
if (generate_new_revision) {
if (previous_revision && doc._revs_info.length === 0) {
// the document history has changed, it means that the document
// revision was wrong. Add a pseudo history to the document
doc._rev = previous_revision;
doc._revs = {
"start": parseInt(previous_revision.split("-")[0], 10),
"ids": [previous_revision.split("-")[1]]
};
doc._revs_info = [{"rev": previous_revision, "status": "missing"}];
}
doc = priv.generateNextRevision(
doc,
specific_parameter.remove
);
}
};
f.updateRevsInfo = function () {
if (doc._revs) {
revs_info = priv.updateDocumentTree(doctree, doc._revs);
} else {
revs_info = priv.postToDocumentTree(doctree, doc);
if (doc._revs_info.length > 1) {
prev_doc = {
"_id": doc._id,
"_rev": doc._revs_info[1].rev
};
}
// force revs_info status
doc._revs_info[0].status = (specific_parameter.remove ?
"deleted" : "available");
priv.updateDocumentTree(doc, doc_tree);
if (prev_doc) {
return priv.getDocument(prev_doc, option, callback.getDocument);
}
if (specific_parameter.remove || specific_parameter.removeAttachment) {
console.log("this one");
return onEnd(priv.notFoundError(
"Unable to remove an inexistent document",
"missing"
), undefined);
}
priv.putDocument(doc, option, callback.putDocument);
};
f.postDocument = function (attachment_list) {
doc._id = docid + "." + revs_info[0].rev;
delete doc._rev;
delete doc._revs;
that.addJob(
"post",
priv.substorage,
doc,
command.cloneOption(),
function () {
var i;
if (attachment_list.length === 0) {
f.sendDocumentTree();
} else {
f.send_document_tree_count = attachment_list.length;
for (i = 0; i < attachment_list.length; i += 1) {
f.copyAttachment(attachment_list[i].id,
attachment_list[i].attachment);
}
callback.getDocument = function (err, res_doc) {
console.log("c getDocument");
var k, conflicts;
if (err) {
if (err.status === 404) {
if (specific_parameter.remove ||
specific_parameter.removeAttachment) {
return onEnd(priv.conflictError(
"Document update conflict",
"Document is missing"
), undefined);
}
},
function (err) {
switch (err.status) {
case 409:
// file already exists
f.sendDocumentTree();
break;
default:
err.message = "Cannot upload document";
f.error(err);
break;
if (specific_parameter.get) {
return onEnd(priv.notFoundError(
"Unable to find the document",
"missing"
), undefined);
}
res_doc = {};
} else {
err.message = "Cannot get document";
return onEnd(err, undefined);
}
);
};
f.copyAttachment = function (attachmentid, attachment) {
that.addJob(
"get",
priv.substorage,
prev_doc._id + "/" + attachmentid,
command.cloneOption(),
function (response) {
attachment._data = response;
that.addJob(
"putAttachment",
priv.substorage,
attachment,
command.cloneOption(),
function (response) {
f.sendDocumentTree();
},
function (err) {
err.message = "Cannot copy previous attachment";
f.error(err);
}
);
},
function (err) {
err.message = "Cannot get previous attachment";
f.error(err);
}
);
};
f.send_document_tree_count = 0;
f.sendDocumentTree = function () {
f.send_document_tree_count -= 1;
if (f.send_document_tree_count > 0) {
return;
}
doctree._id = docid + priv.doctree_suffix;
that.addJob(
"put",
priv.substorage,
doctree,
command.cloneOption(),
function () {
that.success({
"ok": true,
"id": docid,
"rev": revs_info[0].rev
});
},
function (err) {
// xxx do we try to delete the posted document ?
err.message = "Cannot save document revision tree";
f.error(err);
if (specific_parameter.get) {
res_doc._id = doc._id;
res_doc._rev = doc._rev;
if (option.conflicts === true) {
conflicts = priv.getConflicts(doc._rev, doc_tree);
if (conflicts) {
res_doc._conflicts = conflicts;
}
}
);
};
f.error = function (err) {
f.error = function () {};
that.error(err);
};
f.getDocumentTree();
};
/**
* Update the document metadata and update a document tree.
* Options:
* - {boolean} keep_revision_history To keep the previous revisions
* (false by default) (NYI).
* @method put
* @param {object} command The JIO command
*/
that.put = function (command) {
that.post(command);
};
/**
* Create/Update the document attachment and update a document tree.
* Options:
* - {boolean} keep_revision_history To keep the previous revisions
* (false by default) (NYI).
* @method putAttachment
* @param {object} command The JIO command
*/
that.putAttachment = function (command) {
var functions = {}, doc, doctree, revs_info, prev_doc;
doc = command.cloneDoc();
functions.begin = function () {
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;
}
functions.getDocumentTree();
};
functions.getDocumentTree = function () {
var option = command.cloneOption();
if (option.max_retry === 0) {
option.max_retry = 3;
if (option.revs === true) {
res_doc._revisions = doc._revs;
}
if (option.revs_info === true) {
res_doc._revs_info = doc._revs_info;
}
return onEnd(undefined, res_doc);
}
that.addJob(
"get",
priv.substorage,
command.getDocId() + priv.doctree_suffix,
option,
function (response) {
doctree = response;
functions.updateRevsInfo();
functions.getDocument();
},
function (err) {
switch (err.status) {
case 404:
doctree = priv.createDocumentTree();
functions.updateRevsInfo();
functions.getDocument();
break;
default:
err.message = "Cannot get document revision tree";
that.error(err);
break;
if (specific_parameter.removeAttachment) {
// copy metadata (not beginning by "_" to document
for (k in res_doc) {
if (res_doc.hasOwnProperty(k) && !k.match("^_")) {
doc[k] = res_doc[k];
}
}
);
};
functions.updateRevsInfo = function () {
if (doc._revs) {
revs_info = priv.updateDocumentTree(doctree, doc._revs);
} else {
revs_info = priv.postToDocumentTree(doctree, doc);
}
};
functions.postEmptyDocument = function () {
that.addJob(
"post",
priv.substorage,
{"_id": command.getDocId() + "." + revs_info[0].rev},
command.getOption(),
function (response) {
doc._rev = response.rev;
functions.postAttachment();
},
function (err) {
err.message = "Cannot upload document";
that.error(err);
}
);
};
functions.getDocument = function () {
if (revs_info[1] === undefined) {
functions.postEmptyDocument();
if (specific_parameter.remove) {
priv.putDocumentTree(doc, option, doc_tree, callback.putDocumentTree);
} else {
that.addJob(
"get",
priv.substorage,
command.getDocId() + "." + revs_info[1].rev,
command.getOption(),
function (response) {
var attachment_list = [], i;
prev_doc = response;
for (i in response._attachments) {
if (response._attachments.hasOwnProperty(i)) {
attachment_list.push({"id": i, "attachment": {
"_id": command.getDocId() + "." + revs_info[0].rev + "/" + i,
"_mimetype": response._attachments[i].content_type,
"_data": undefined
}});
}
}
functions.postDocument(attachment_list);
},
function (err) {
if (err.status === 404) {
functions.postDocument([]);
return;
}
err.message = "Cannot upload document";
that.error(err);
}
);
console.log("res_doc");
console.log(res_doc);
priv.getAttachmentList(res_doc, option, callback.getAttachmentList);
}
};
functions.postDocument = function (attachment_list) {
that.addJob(
"post",
priv.substorage,
command.getDocId() + "." + revs_info[0].rev,
command.getOption(),
function (response) {
var i;
if (attachment_list.length === 0) {
functions.postAttachment();
} else {
functions.post_attachment_count = attachment_list.length;
for (i = 0; i < attachment_list.length; i += 1) {
functions.copyAttachment(attachment_list[i].id,
attachment_list[i].attachment);
}
callback.getAttachmentList = function (err, res_list) {
console.log("c getAttachmentList");
var i, attachment_found = false;
if (err) {
err.message = "Cannot get attachment";
return onEnd(err, undefined);
}
attachment_list = res_list || [];
if (specific_parameter.getAttachment) {
// getting specific attachment
for (i = 0; i < attachment_list.length; i += 1) {
if (attachment_list[i] &&
doc._attachment ===
attachment_list[i]._attachment) {
return onEnd(attachment_list[i]._data);
}
},
function (err) {
err.message = "Cannot upload document";
that.error(err);
}
);
};
functions.copyAttachment = function (attachmentid, attachment) {
that.addJob(
"get",
priv.substorage,
prev_doc._id + "/" + attachmentid,
command.cloneOption(),
function (response) {
attachment._data = response;
that.addJob(
"putAttachment",
priv.substorage,
attachment,
command.cloneOption(),
function (response) {
functions.postAttachment();
},
function (err) {
err.message = "Cannot copy previous attachment";
functions.error(err);
}
);
},
function (err) {
err.message = "Cannot copy previous attachment";
functions.error(err);
return onEnd(priv.notFoundError(
"Unable to get an inexistent attachment",
"missing"
), undefined);
}
if (specific_parameter.remove_from_attachment_list) {
// removing specific attachment
for (i = 0; i < attachment_list.length; i += 1) {
if (attachment_list[i] &&
specific_parameter.remove_from_attachment_list._attachment ===
attachment_list[i]._attachment) {
attachment_found = true;
attachment_list[i] = undefined;
break;
}
}
if (!attachment_found) {
return onEnd(priv.notFoundError(
"Unable to remove an inexistent attachment",
"missing"
), undefined);
}
);
};
functions.post_attachment_count = 0;
functions.postAttachment = function () {
functions.post_attachment_count -= 1;
if (functions.post_attachment_count > 0) {
return;
}
that.addJob(
"putAttachment",
priv.substorage,
{
"_id": command.getDocId() + "." + revs_info[0].rev + "/" +
command.getAttachmentId(),
"_mimetype": command.getAttachmentMimeType(),
"_data": command.getAttachmentData()
},
command.cloneOption(),
function () {
functions.sendDocumentTree();
},
function (err) {
switch (err.status) {
case 409:
// file already exists
functions.sendDocumentTree();
break;
default:
err.message = "Cannot upload attachment";
functions.error(err);
priv.putDocument(doc, option, callback.putDocument);
};
callback.putDocument = function (err, response) {
console.log("c putDocument");
var i, attachment_found = false;
if (err) {
err.message = "Cannot post the document";
return onEnd(err, undefined);
}
if (specific_parameter.add_to_attachment_list) {
// adding specific attachment
attachment_list = attachment_list || [];
for (i = 0; i < attachment_list.length; i += 1) {
if (specific_parameter.add_to_attachment_list._attachment ===
attachment_list[i]._attachment) {
attachment_found = true;
attachment_list[i] = specific_parameter.add_to_attachment_list;
break;
}
}
);
};
functions.sendDocumentTree = function () {
doctree._id = command.getDocId() + priv.doctree_suffix;
that.addJob(
"put",
priv.substorage,
doctree,
command.cloneOption(),
function () {
that.success({
"ok": true,
"id": command.getDocId() + "/" + command.getAttachmentId(),
"rev": revs_info[0].rev
});
},
function (err) {
// xxx do we try to delete the posted document ?
err.message = "Cannot save document revision tree";
functions.error(err);
if (!attachment_found) {
attachment_list.unshift(specific_parameter.add_to_attachment_list);
}
}
priv.putAttachmentList(
doc,
option,
attachment_list,
callback.putAttachmentList
);
};
functions.error = function (err) {
functions.error = function () {};
that.error(err);
callback.putAttachmentList = function (err, response) {
console.log("c putAttachmentList");
if (err) {
err.message = "Cannot copy attacments to the document";
return onEnd(err, undefined);
}
priv.putDocumentTree(doc, option, doc_tree, callback.putDocumentTree);
};
callback.putDocumentTree = function (err, response) {
console.log("c putDocumentTree");
if (err) {
err.message = "Cannot update the document history";
return onEnd(err, undefined);
}
onEnd(undefined, {
"ok": true,
"id": doc._id + (specific_parameter.putAttachment ||
specific_parameter.removeAttachment ||
specific_parameter.getAttachment ?
"/" + doc._attachment : ""),
"rev": doc._rev
});
// if (option.keep_revision_history !== true) {
// // priv.remove(prev_doc, option, function () {
// // - change "available" status to "deleted"
// // - remove attachments
// // - done, no callback
// // });
// }
};
functions.begin();
callback.begin();
};
/**
* Get the document metadata or attachment.
* Post the document metadata and create or update a document tree.
* Options:
* - {boolean} revs Add simple revision history (false by default).
* - {boolean} revs_info Add revs info (false by default).
* - {boolean} conflicts Add conflict object (false by default).
* @method get
* - {boolean} keep_revision_history To keep the previous revisions
* (false by default) (NYI).
* @method post
* @param {object} command The JIO command
*/
that.get = function (command) {
var f = {}, doctree, revs_info, prev_rev, option;
option = command.cloneOption();
if (option.max_retry === 0) {
option.max_retry = 3;
}
prev_rev = command.getDocInfo("_rev");
if (typeof prev_rev === "string") {
if (!priv.checkRevisionFormat(prev_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;
}
}
f.getDocumentTree = function () {
that.addJob(
"get",
priv.substorage,
{
"_id": command.getDocId() + priv.doctree_suffix,
"_rev": command.getDocInfo("_rev")
},
option,
function (response) {
doctree = response;
if (prev_rev === undefined) {
revs_info = priv.getWinnerRevisionFromDocumentTree(doctree);
if (revs_info.length > 0) {
prev_rev = revs_info[0].rev;
} else {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the document",
"reason": "Document is deleted"
});
return;
}
} else {
revs_info = priv.getRevisionFromDocumentTree(doctree, prev_rev);
}
f.getDocument(command.getDocId() + "." + prev_rev,
command.getAttachmentId());
},
function (err) {
switch (err.status) {
case 404:
that.error(err);
break;
default:
err.message = "Cannot get document revision tree";
that.error(err);
break;
}
}
);
};
f.getDocument = function (docid, attmtid) {
that.addJob(
"get",
priv.substorage,
{"_id": docid, "_rev": command.getDocInfo("_rev")},
option,
function (response) {
var attmt;
if (typeof response !== "string") {
if (attmtid !== undefined) {
if (response._attachments !== undefined) {
attmt = response._attachments[attmtid];
if (attmt !== undefined) {
prev_rev = priv.getRevisionFromPosition(
revs_info,
attmt.revpos
);
f.getDocument(command.getDocId() + "." + prev_rev + "/" +
attmtid);
return;
}
}
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Cannot find the attachment",
"reason": "Attachment is missing"
});
return;
}
response._id = command.getDocId();
response._rev = prev_rev;
if (command.getOption("revs") === true) {
response._revisions = priv.revsInfoToHistory(revs_info);
}
if (command.getOption("revs_info") === true) {
response._revs_info = revs_info;
}
if (command.getOption("conflicts") === true) {
response._conflicts = priv.getLeavesFromDocumentTree(
doctree,
prev_rev
);
if (response._conflicts.length === 0) {
delete response._conflicts;
}
}
}
that.success(response);
},
function (err) {
that.error(err);
that.post = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{},
function (err, response) {
if (err) {
return that.error(err);
}
);
};
if (command.getAttachmentId() && prev_rev !== undefined) {
f.getDocument(command.getDocId() + "." + prev_rev +
"/" + command.getAttachmentId());
} else {
f.getDocumentTree();
}
that.success(response);
}
);
};
/**
* Remove document or attachment.
* Put the document metadata and create or update a document tree.
* Options:
* - {boolean} keep_revision_history To keep the previous revisions
* @method remove
* (false by default) (NYI).
* @method put
* @param {object} command The JIO command
*/
that.remove = function (command) {
var f = {}, del_rev, option, new_doc, revs_info;
option = command.cloneOption();
if (option.max_retry === 0) {
option.max_retry = 3;
}
del_rev = command.getDoc()._rev;
that.put = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
f.removeDocument = function (docid, doctree) {
if (command.getOption("keep_revision_history") !== true) {
if (command.getAttachmentId() === undefined) {
// update tree
revs_info = priv.postToDocumentTree(
doctree,
command.getDoc(),
true
);
// remove revision
that.addJob(
"remove",
priv.substorage,
docid,
option,
function () {
// put tree
doctree._id = command.getDocId() + priv.doctree_suffix;
that.addJob(
"put",
priv.substorage,
doctree,
command.cloneOption(),
function () {
that.success({
"ok": true,
"id": command.getDocId(),
"rev": revs_info[0].rev
});
},
function () {
that.error({
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": "Document update conflict.",
"reason": "Cannot update document tree"
});
return;
}
);
},
function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "File not found",
"reason": "Document was not found"
});
return;
}
);
} else {
// get previsous document
that.addJob(
"get",
priv.substorage,
command.getDocId() + "." + del_rev,
option,
function (response) {
// update tree
revs_info = priv.postToDocumentTree(doctree, command.getDoc());
new_doc = response;
delete new_doc._attachments;
new_doc._id = new_doc._id + "." + revs_info[0].rev;
// post new document version
that.addJob(
"post",
priv.substorage,
new_doc,
command.cloneOption(),
function () {
// put tree
doctree._id = command.getDocId() + priv.doctree_suffix;
that.addJob(
"put",
priv.substorage,
doctree,
command.cloneOption(),
function () {
that.success({
"ok": true,
"id": new_doc._id,
"rev": revs_info[0].rev
});
},
function (err) {
err.message =
"Cannot save document revision tree";
that.error(err);
}
);
},
function () {
that.error({
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": "Document update conflict.",
"reason": "Cannot update document"
});
return;
}
);
},
function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "File not found",
"reason": "Document was not found"
});
return;
}
);
that.putAttachment = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"doc_id": command.getDocId(),
"attachment_id": command.getAttachmentId(),
"add_to_attachment_list": {
"_attachment": command.getAttachmentId(),
"_mimetype": command.getAttachmentMimeType(),
"_data": command.getAttachmentData()
},
"putAttachment": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
};
if (typeof del_rev === "string") {
if (!priv.checkRevisionFormat(del_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;
}
}
);
};
// get doctree
that.addJob(
"get",
priv.substorage,
command.getDocId() + priv.doctree_suffix,
option,
function (response) {
response._conflicts = priv.getLeavesFromDocumentTree(response);
that.remove = function (command) {
if (command.getAttachmentId()) {
return that.removeAttachment(command);
}
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"revision_needed": true,
"remove": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
if (del_rev === undefined) {
// no revision provided
that.error({
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": "Document update conflict.",
"reason": "Cannot delete a document without revision"
});
return;
that.removeAttachment = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"doc_id": command.getDocId(),
"attachment_id": command.getAttachmentId(),
"revision_needed": true,
"removeAttachment": true,
"remove_from_attachment_list": {
"_attachment": command.getAttachmentId()
}
// revision provided
if (priv.isRevisionALeaf(response, del_rev) === true) {
if (typeof command.getAttachmentId() === "string") {
f.removeDocument(command.getDocId() + "." + del_rev +
"/" + command.getAttachmentId(), response);
} else {
f.removeDocument(command.getDocId() + "." + del_rev,
response);
}
} else {
that.error({
"status": 409,
"statusText": "Conflict",
"error": "conflict",
"message": "Document update conflict.",
"reason": "Trying to remove non-latest revision"
});
return;
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
that.get = function (command) {
if (command.getAttachmentId()) {
return that.getAttachment(command);
}
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"get": true
},
function () {
that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Document tree not found, please checkdocument ID",
"reason": "Incorrect document ID"
});
return;
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
/**
* Get all documents
* @method allDocs
* @param {object} command The JIO command
*/
that.allDocs = function () {
setTimeout(function () {
that.error({
"status": 405,
"statusText": "Method Not Allowed",
"error": "method_not_allowed",
"message": "Your are not allowed to use this command",
"reason": "LocalStorage forbids AllDocs command executions"
});
});
that.getAttachment = function (command) {
priv.revisionGenericRequest(
command.cloneDoc(),
command.cloneOption(),
{
"doc_id": command.getDocId(),
"attachment_id": command.getAttachmentId(),
"getAttachment": true
},
function (err, response) {
if (err) {
return that.error(err);
}
that.success(response);
}
);
};
// END //
priv.RevisionStorage();
return that;
});
}); // end RevisionStorage
......@@ -34,9 +34,14 @@ clone = function (obj) {
// generates a revision hash from document metadata, revision history
// and the deleted_flag
generateRevisionHash = function (doc, revisions, deleted_flag) {
var string = JSON.stringify(doc) + JSON.stringify(revisions) +
JSON.stringify(deleted_flag? true: false);
return hex_sha256(string);
var string;
doc = clone(doc);
delete doc._rev;
delete doc._revs;
delete doc._revs_info;
string = JSON.stringify(doc) + JSON.stringify(revisions) +
JSON.stringify(deleted_flag? true: false);
return hex_sha256(string);
},
// localStorage wrapper
localstorage = {
......@@ -1264,7 +1269,8 @@ test ("Post", function(){
o.doc = {"_id": "post1", "_rev": o.rev, "title": "myPost2"};
o.revisions = {"start": 1, "ids": [o.rev.split('-')[1]]};
o.rev = "2-"+generateRevisionHash(o.doc, o.revisions);
o.spy (o, "status", undefined, "Post + revision");
o.spy (o, "value", {"ok": true, "id": "post1", "rev": o.rev},
"Post + revision");
o.jio.post(o.doc, o.f);
o.tick(o);
......@@ -1293,8 +1299,92 @@ test ("Post", function(){
"Check document tree"
);
o.jio.stop();
// add attachment
o.doc._attachments = {
"attachment_test": {
"length": 35,
"digest": "A",
"content_type": "oh/yeah"
}
};
localstorage.setItem(o.localpath + "/post1." + o.rev, o.doc);
localstorage.setItem(o.localpath + "/post1." + o.rev + "/attachment_test",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
// post + attachment copy
o.doc = {"_id": "post1", "_rev": o.rev, "title": "myPost2"};
o.revisions = {
"start": 2,
"ids": [o.rev.split('-')[1], o.revisions.ids[0]]
};
o.rev = "3-"+generateRevisionHash(o.doc, o.revisions);
o.spy (o, "value", {"ok": true, "id": "post1", "rev": o.rev},
"Post + attachment copy");
o.jio.post(o.doc, o.f);
o.tick(o);
// check attachment
deepEqual(
localstorage.getItem(o.localpath + "/post1." + o.rev +
"/attachment_test"),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"Check Attachment"
);
// check document tree
o.doc_tree._id = "post1.revision_tree.json";
o.doc_tree.children[0].children[0].children.unshift({
"rev": o.rev, "status": "available", "children": []
});
deepEqual(
localstorage.getItem(
o.localpath + "/post1.revision_tree.json"
),
o.doc_tree,
"Check document tree"
);
// post + wrong revision
o.doc = {"_id": "post1", "_rev": "3-wr3", "title": "myPost3"};
o.revisions = {"start": 3, "ids": ["wr3"]};
o.rev = "4-"+generateRevisionHash(o.doc, o.revisions);
o.spy(o, "value", {"id": "post1", "ok": true, "rev": o.rev},
"Postt + wrong revision");
o.jio.post(o.doc, o.f);
o.tick(o);
// check document
deepEqual(
localstorage.getItem(o.localpath + "/post1.3-wr3"),
null,
"Check document"
);
// check document
o.doc._id = "post1." + o.rev;
delete o.doc._rev;
deepEqual(
localstorage.getItem(o.localpath + "/post1." + o.rev),
o.doc,
"Check document"
);
// check document tree
o.doc_tree._id = "post1.revision_tree.json";
o.doc_tree.children.unshift({
"rev": "3-wr3", "status": "missing", "children": [{
"rev": o.rev, "status": "available", "children": []
}]
});
deepEqual(
localstorage.getItem(
o.localpath + "/post1.revision_tree.json"
),
o.doc_tree,
"Check document tree"
);
o.jio.stop();
});
test ("Put", function(){
......@@ -1562,7 +1652,7 @@ test("Put Attachment", function () {
// putAttachment without document
o.revisions = {"start": 0, "ids": []}
o.rev_hash = generateRevisionHash({"_id": "doc1/attmt1"},
o.rev_hash = generateRevisionHash({"_id": "doc1", "_attachment": "attmt1"},
o.revisions);
o.rev = "1-" + o.rev_hash;
o.spy(o, "value", {"ok": true, "id": "doc1/attmt1", "rev": o.rev},
......@@ -1601,9 +1691,9 @@ test("Put Attachment", function () {
o.prev_rev = o.rev;
o.revisions = {"start": 1, "ids": [o.rev_hash]}
o.rev_hash = generateRevisionHash({
"_id": "doc1/attmt1",
"_id": "doc1",
"_data": "abc",
"_rev": o.prev_rev
"_attachment": "attmt1",
}, o.revisions);
o.rev = "2-" + o.rev_hash;
o.spy(o, "value", {"ok": true, "id": "doc1/attmt1", "rev": o.rev},
......@@ -1646,9 +1736,9 @@ test("Put Attachment", function () {
o.prev_rev = o.rev;
o.revisions = {"start": 2, "ids": [o.rev_hash, o.revisions.ids[0]]}
o.rev_hash = generateRevisionHash({
"_id": "doc1/attmt2",
"_id": "doc1",
"_data": "def",
"_rev": o.prev_rev
"_attachment": "attmt2",
}, o.revisions);
o.rev = "3-" + o.rev_hash;
o.spy(o, "value", {"ok": true, "id": "doc1/attmt2", "rev": o.rev},
......@@ -1723,15 +1813,16 @@ test ("Get", function(){
o.doctree = {"children":[{
"rev": "1-rev1", "status": "available", "children": []
}]};
o.doc_myget1 = {"_id": "get1", "title": "myGet1"};
o.doc_myget1 = {"_id": "get1.1-rev1", "title": "myGet1"};
localstorage.setItem(o.localpath+"/get1.revision_tree.json", o.doctree);
localstorage.setItem(o.localpath+"/get1.1-rev1", o.doc_myget1);
// get document
o.doc_myget1_cloned = clone(o.doc_myget1);
o.doc_myget1_cloned["_rev"] = "1-rev1";
o.doc_myget1_cloned["_revisions"] = {"start": 1, "ids": ["rev1"]};
o.doc_myget1_cloned["_revs_info"] = [{
o.doc_myget1_cloned._id = "get1";
o.doc_myget1_cloned._rev = "1-rev1";
o.doc_myget1_cloned._revisions = {"start": 1, "ids": ["rev1"]};
o.doc_myget1_cloned._revs_info = [{
"rev": "1-rev1", "status": "available"
}];
o.spy(o, "value", o.doc_myget1_cloned, "Get document (winner)");
......@@ -1748,14 +1839,15 @@ test ("Get", function(){
"rev": "2-rev3", "status": "available", "children": []
}]
}]};
o.doc_myget2 = {"_id": "get1", "title": "myGet2"};
o.doc_myget3 = {"_id": "get1", "title": "myGet3"};
o.doc_myget2 = {"_id": "get1.1-rev2", "title": "myGet2"};
o.doc_myget3 = {"_id": "get1.2-rev3", "title": "myGet3"};
localstorage.setItem(o.localpath+"/get1.revision_tree.json", o.doctree);
localstorage.setItem(o.localpath+"/get1.1-rev2", o.doc_myget2);
localstorage.setItem(o.localpath+"/get1.2-rev3", o.doc_myget3);
// get document
o.doc_myget3_cloned = clone(o.doc_myget3);
o.doc_myget3_cloned._id = "get1";
o.doc_myget3_cloned["_rev"] = "2-rev3";
o.doc_myget3_cloned["_revisions"] = {"start": 2, "ids": ["rev3","rev2"]};
o.doc_myget3_cloned["_revs_info"] = [{
......@@ -1780,6 +1872,7 @@ test ("Get", function(){
// get specific document
o.doc_myget2_cloned = clone(o.doc_myget2);
o.doc_myget2_cloned._id = "get1";
o.doc_myget2_cloned["_rev"] = "1-rev2";
o.doc_myget2_cloned["_revisions"] = {"start": 1, "ids": ["rev2"]};
o.doc_myget2_cloned["_revs_info"] = [{
......@@ -1793,18 +1886,16 @@ test ("Get", function(){
o.tick(o);
// adding an attachment
o.attmt_myget2 = {
o.attmt_myget3 = {
"get2": {
"length": 3,
"digest": "md5-dontcare",
"revpos": 1
"content_type": "oh/yeah"
}
};
o.doc_myget2["_attachments"] = o.attmt_myget2;
o.doc_myget3["_attachments"] = o.attmt_myget2;
localstorage.setItem(o.localpath+"/get1.1-rev2", o.doc_myget2);
o.doc_myget3._attachments = o.attmt_myget3;
localstorage.setItem(o.localpath+"/get1.2-rev3", o.doc_myget3);
localstorage.setItem(o.localpath+"/get1.1-rev2/get2", "abc");
localstorage.setItem(o.localpath+"/get1.2-rev3/get2", "abc");
// get attachment winner
o.spy(o, "value", "abc", "Get attachment (winner)");
......@@ -1820,13 +1911,13 @@ test ("Get", function(){
// get attachment specific rev
o.spy(o, "value", "abc", "Get attachment (specific revision)");
o.jio.get({"_id": "get1/get2", "_rev": "1-rev2"}, {
o.jio.get({"_id": "get1/get2", "_rev": "2-rev3"}, {
"revs_info": true, "revs": true, "conflicts": true,
}, o.f);
o.tick(o);
// get document with attachment (specific revision)
o.doc_myget2_cloned["_attachments"] = o.attmt_myget2;
delete o.doc_myget2_cloned._attachments;
o.spy(o, "value", o.doc_myget2_cloned,
"Get document which have an attachment (specific revision)");
o.jio.get({"_id": "get1", "_rev": "1-rev2"}, {
......@@ -1835,7 +1926,7 @@ test ("Get", function(){
o.tick(o);
// get document with attachment (winner)
o.doc_myget3_cloned["_attachments"] = o.attmt_myget2;
o.doc_myget3_cloned._attachments = o.attmt_myget3;
o.spy(o, "value", o.doc_myget3_cloned,
"Get document which have an attachment (winner)");
o.jio.get({"_id": "get1"},
......@@ -1862,188 +1953,135 @@ test ("Remove", function(){
o.localpath = "jio/localstorage/urevrem/arevrem";
// 1. remove document without revision
o.spy (o, "status", 404,
"Remove document (no doctree, no revision)");
o.spy(o, "status", 409, "Remove document without revision " +
"-> 409 Conflict");
o.jio.remove({"_id":"remove1"}, o.f);
o.tick(o);
// 2. remove attachment without revision
o.spy (o, "status", 404,
"Remove attachment (no doctree, no revision)");
o.spy(o, "status", 409, "Remove attachment without revision " +
"-> 409 Conflict");
o.jio.remove({"_id":"remove1/remove2"}, o.f);
o.tick(o);
// adding two documents
o.doc_myremove1 = {"_id": "remove1", "title": "myRemove1"};
o.doc_myremove2 = {"_id": "remove1", "title": "myRemove2"};
o.very_old_rev = "1-veryoldrev";
// adding a document with attachments
o.doc_myremove1 = {
"_id": "remove1.1-veryoldrev",
"title": "myRemove1"
};
localstorage.setItem(o.localpath+"/remove1."+o.very_old_rev,
localstorage.setItem(o.localpath + "/remove1.1-veryoldrev",
o.doc_myremove1);
localstorage.setItem(o.localpath+"/remove1.1-rev2", o.doc_myremove1);
// add attachment
o.attmt_myremove1 = {
"remove2": {
"length": 3,
"digest": "md5-dontcare",
"revpos":1
},
o.doc_myremove1._id = "remove1.2-oldrev";
o.attachment_remove2 = {
"length": 3,
"digest": "md5-dontcare",
"content_type": "oh/yeah"
}
o.attachment_remove3 = {
"length": 5,
"digest": "md5-865f5cc7fbd7854902eae9d8211f178a",
"content_type": "he/ho"
}
o.doc_myremove1._attachments = {
"remove2": o.attachment_remove2,
"remove3": o.attachment_remove3
};
o.doc_myremove1 = {"_id": "remove1", "title": "myRemove1",
"_attachments":o.attmt_myremove1};
o.revisions = {"start":1,"ids":[o.very_old_rev.split('-'),[1]]}
o.old_rev = "2-"+generateRevisionHash(o.doc_myremove1, o.revisions);
localstorage.setItem(o.localpath+"/remove1."+o.old_rev, o.doc_myremove1);
localstorage.setItem(o.localpath+"/remove1."+o.old_rev+"/remove2", "xyz");
localstorage.setItem(o.localpath + "/remove1.2-oldrev",
o.doc_myremove1);
localstorage.setItem(o.localpath + "/remove1.2-oldrev/remove2", "abc");
localstorage.setItem(o.localpath + "/remove1.2-oldrev/remove3", "defgh");
o.doctree = {"children":[{
"rev": o.very_old_rev, "status": "available", "children": [{
"rev": o.old_rev, "status": "available", "children": []
// add document tree
o.doctree = {
"children": [{
"rev": "1-veryoldrev", "status": "available", "children": [{
"rev": "2-oldrev", "status": "available", "children": []
}]
},{
"rev": "1-rev2", "status": "available", "children": []
}]};
localstorage.setItem(o.localpath+"/remove1.revision_tree.json", o.doctree);
// 3. remove non existing attachment with revision
o.spy(o, "status", 404,
"Remove NON-existing attachment (revision)");
o.jio.remove({"_id":"remove1.1-rev2/remove0","_rev":o.old_rev}, o.f);
o.tick(o);
o.revisions = {"start": 2, "ids":[
o.old_rev.split('-')[1], o.very_old_rev.split('-')[1]
]};
o.doc_myremove1 = {"_id":"remove1/remove2","_rev":o.old_rev};
o.rev = "3-"+generateRevisionHash(o.doc_myremove1, o.revisions);
}]
};
localstorage.setItem(o.localpath + "/remove1.revision_tree.json",
o.doctree);
// 4. remove existing attachment with revision
o.spy (o, "value", {"ok": true, "id": "remove1."+o.rev, "rev": o.rev},
"Remove existing attachment (revision)");
o.jio.remove({"_id":"remove1/remove2","_rev":o.old_rev}, o.f);
// 3. remove inexistent attachment
o.spy(o, "status", 404, "Remove inexistent attachment -> 404 Not Found");
o.jio.remove({"_id": "remove1/remove0", "_rev": "2-oldrev"}, o.f);
o.tick(o);
o.testtree = {"children":[{
"rev": o.very_old_rev, "status": "available", "children": [{
"rev": o.old_rev, "status": "available", "children": [{
"rev": o.rev, "status": "available", "children": []
}]
// 4. remove existing attachment
o.rev_hash = generateRevisionHash({
"_id": "remove1",
"_attachment": "remove2",
}, {"start": 2, "ids": ["oldrev", "veryoldrev"]});
o.spy (o, "value",
{"ok": true, "id": "remove1/remove2", "rev": "3-" + o.rev_hash},
"Remove existing attachment");
o.jio.remove({"_id":"remove1/remove2", "_rev": "2-oldrev"}, o.f);
o.tick(o);
o.doctree = {
"children":[{
"rev": "1-veryoldrev", "status": "available", "children": [{
"rev": "2-oldrev", "status": "available", "children": [{
"rev": "3-" + o.rev_hash, "status": "available", "children": []
}]
}]
},{
"rev": "1-rev2", "status": "available", "children": []
}]};
}]
};
// 5. check if document tree has been updated correctly
deepEqual(localstorage.getItem(
"jio/localstorage/urevrem/arevrem/remove1.revision_tree.json"
),o.testtree, "Check document tree");
o.localpath + "/remove1.revision_tree.json"
), o.doctree, "Check document tree");
// 6. check if attachment has been removed
// 6. check if the attachment still exists
deepEqual(localstorage.getItem(
"jio/localstorage/urevrem/arevrem/remove1."+o.rev+"/remove2"
), null, "Check attachment");
o.localpath + "/remove1.2-oldrev/remove2"
), "abc", "Check attachment -> still exists");
// 7. check if document is updated
deepEqual(localstorage.getItem(
"jio/localstorage/urevrem/arevrem/remove1."+o.rev
), {"_id": "remove1."+o.rev, "title":"myRemove1"}, "Check document");
// add another attachment
o.attmt_myremove2 = {
"remove3": {
"length": 3,
"digest": "md5-hello123"
},
"revpos":1
};
o.doc_myremove2 = {"_id": "remove1", "title": "myRemove2",
"_attachments":o.attmt_myremove2};
o.revisions = {"start":1,"ids":["rev2"] };
o.second_old_rev = "2-"+generateRevisionHash(o.doc_myremove2, o.revisions);
localstorage.setItem(o.localpath+"/remove1."+o.second_old_rev,
o.doc_myremove2);
localstorage.setItem(o.localpath+"/remove1."+o.second_old_rev+"/remove3",
"stu");
o.doctree = {"children":[{
"rev": o.very_old_rev, "status": "available", "children": [{
"rev": o.old_rev, "status": "available", "children": [{
"rev": o.rev, "status": "available", "children":[]
}]
}]
},{
"rev": "1-rev2", "status": "available", "children": [{
"rev": o.second_old_rev, "status": "available", "children":[]
}]
}]};
localstorage.setItem(o.localpath+"/remove1.revision_tree.json", o.doctree);
// 8. remove non existing attachment without revision
o.spy (o,"status", 409,
"409 - Removing non-existing-attachment (no revision)");
o.jio.remove({"_id":"remove1/remove0"}, o.f);
o.tick(o);
o.revisions = {"start":2,"ids":[o.second_old_rev.split('-')[1],"rev2"]};
o.doc_myremove3 = {"_id":"remove1/remove3","_rev":o.second_old_rev};
o.second_rev = "3-"+generateRevisionHash(o.doc_myremove3, o.revisions);
// 9. remove existing attachment without revision
o.spy (o,"status", 409, "409 - Removing existing attachment (no revision)");
o.jio.remove({"_id":"remove1/remove3"}, o.f);
o.tick(o);
// 10. remove wrong revision
o.spy (o,"status", 409, "409 - Removing document (false revision)");
o.jio.remove({"_id":"remove1","_rev":"1-rev2"}, o.f);
o.tick(o);
o.revisions = {"start": 3, "ids":[
o.rev.split('-')[1],
o.old_rev.split('-')[1],o.very_old_rev.split('-')[1]
]};
o.doc_myremove4 = {"_id":"remove1","_rev":o.rev};
o.second_new_rev = "4-"+
generateRevisionHash(o.doc_myremove4, o.revisions, true);
// 11. remove document version with revision
o.spy (o, "value", {"ok": true, "id": "remove1", "rev":
o.second_new_rev},
"Remove document (with revision)");
o.jio.remove({"_id":"remove1", "_rev":o.rev}, o.f);
o.localpath + "/remove1.3-" + o.rev_hash
), {
"_id": "remove1.3-" + o.rev_hash,
"title":"myRemove1",
"_attachments": {"remove3": o.attachment_remove3}
}, "Check document");
// 8. remove document with wrong revision
o.spy(o, "status", 409, "Remove document with wrong revision " +
"-> 409 Conflict");
o.jio.remove({"_id":"remove1", "_rev": "1-a"}, o.f);
o.tick(o);
// 9. remove attachment wrong revision
o.spy(o, "status", 409, "Remove attachment with wrong revision " +
"-> 409 Conflict");
o.jio.remove({"_id":"remove1/remove2", "_rev": "1-a"}, o.f);
o.tick(o);
// 10. remove document
o.last_rev = "3-" + o.rev_hash;
o.rev_hash = generateRevisionHash(
{"_id": "remove1"},
{"start": 3, "ids": [o.rev_hash, "oldrev", "veryoldrev"]},
true
);
o.spy(o, "value", {"ok": true, "id": "remove1", "rev": "4-" + o.rev_hash},
"Remove document");
o.jio.remove({"_id":"remove1", "_rev": o.last_rev}, o.f);
o.tick(o);
o.testtree["children"][0]["children"][0]["children"][0]["children"].push({
"rev": o.second_new_rev,
"status": "deleted",
"children": []
// 11. check document tree
o.doctree.children[0].children[0].children[0].children.unshift({
"rev": "4-" + o.rev_hash,
"status": "deleted",
"children": []
});
o.testtree["children"][1]["children"].push({
"rev":o.second_old_rev,
"status":"available",
"children":[]
});
deepEqual(localstorage.getItem(
"jio/localstorage/urevrem/arevrem/remove1.revision_tree.json"
), o.testtree, "Check document tree");
deepEqual(localstorage.getItem(
"jio/localstorage/urevrem/arevrem/remove1."+o.second_new_rev+"/remove2"
), null, "Check attachment");
deepEqual(localstorage.getItem(
"jio/localstorage/urevrem/arevrem/remove1."+o.second_new_rev
), null, "Check document");
// remove document without revision
o.spy (o,"status", 409, "409 - Removing document (no revision)");
o.jio.remove({"_id":"remove1"}, o.f);
o.tick(o);
deepEqual(localstorage.getItem(o.localpath + "/remove1.revision_tree.json"),
o.doctree, "Check document tree");
o.jio.stop();
});
......@@ -2196,9 +2234,7 @@ test ("Scenario", function(){
module ("JIO Replicate Revision Storage");
var testReplicateRevisionStorageGenerator = function (
sinon, jio_description, document_name_have_revision
) {
var testReplicateRevisionStorage = function (sinon, jio_description) {
var o = generateTools(sinon), leavesAction, generateLocalPath;
......@@ -2285,223 +2321,223 @@ module ("JIO Replicate Revision Storage");
}, o.f);
o.tick(o);
// post a new document with id
o.doc = {"_id": "doc1", "title": "post new doc with id"};
o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
"Post document (with id)");
o.jio.post(o.doc, o.f);
o.tick(o);
// /
// |
// 1-1
// check document
o.local_rev_hash = generateRevisionHash(o.doc, o.revision);
o.local_rev = "1-" + o.local_rev_hash;
o.specific_rev_hash = o.local_rev_hash;
o.specific_rev = o.local_rev;
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) +
"/doc1" + suffix),
doc, "Check document"
);
});
// get the post document without revision
o.spy(o, "value", {
"_id": "doc1",
"title": "post new doc with id",
"_rev": "1-1",
"_revisions": {"start": 1, "ids": ["1"]},
"_revs_info": [{"rev": "1-1", "status": "available"}]
}, "Get the previous document (without revision)");
o.jio.get({"_id": "doc1"}, {
"conflicts": true,
"revs": true,
"revs_info": true
}, o.f);
o.tick(o);
// post same document without revision
o.doc = {"_id": "doc1", "title": "post same document without revision"};
o.rev = "1-2";
o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
"Post same document (without revision)");
o.jio.post(o.doc, o.f);
o.tick(o);
// /
// / \
// 1-1 1-2
// 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) +
"/doc1" + suffix),
doc, "Check document"
);
});
// post a new revision
o.doc = {"_id": "doc1", "title": "post new revision", "_rev": o.rev};
o.rev = "2-3";
o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
"Post document (with revision)");
o.jio.post(o.doc, o.f);
o.tick(o);
// /
// / \
// 1-1 1-2
// |
// 2-3
// 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.specific_rev_conflict = o.local_rev;
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) +
"/doc1" + suffix),
doc, "Check document"
);
});
// get the post document with revision
o.spy(o, "value", {
"_id": "doc1",
"title": "post same document without revision",
"_rev": "1-2",
"_revisions": {"start": 1, "ids": ["2"]},
"_revs_info": [{"rev": "1-2", "status": "available"}],
"_conflicts": ["1-1"]
}, "Get the previous document (with revision)");
o.jio.get({"_id": "doc1", "_rev": "1-2"}, {
"conflicts": true,
"revs": true,
"revs_info": true,
}, o.f);
o.tick(o);
// get the post document with specific revision
o.spy(o, "value", {
"_id": "doc1",
"title": "post new doc with id",
"_rev": o.specific_rev,
"_revisions": {"start": 1, "ids": [o.specific_rev_hash]},
"_revs_info": [{"rev": o.specific_rev, "status": "available"}],
"_conflicts": [o.specific_rev_conflict]
}, "Get a previous document (with local storage revision)");
o.jio.get({"_id": "doc1", "_rev": o.specific_rev}, {
"conflicts": true,
"revs": true,
"revs_info": true,
}, o.f);
o.tick(o);
// put document without id
o.spy(o, "status", 20, "Put document without id")
o.jio.put({}, o.f);
o.tick(o);
// put document without rev
o.doc = {"_id": "doc1", "title": "put new document"};
o.rev = "1-4";
o.spy(o, "value", {"id": "doc1", "ok": true, "rev": o.rev},
"Put document without rev")
o.jio.put(o.doc, o.f);
o.tick(o);
// __/__
// / | \
// 1-1 1-2 1-4
// |
// 2-3
// put new revision
o.doc = {"_id": "doc1", "title": "put new revision", "_rev": "1-4"};
o.rev = "2-5";
o.spy(o, "value", {"id": "doc1", "ok": true, "rev": o.rev},
"Put document without rev")
o.jio.put(o.doc, o.f);
o.tick(o);
// __/__
// / | \
// 1-1 1-2 1-4
// | |
// 2-3 2-5
// putAttachment to inexistent document
// putAttachment
// get document
// get attachment
// put document
// get document
// get attachment
// remove attachment
// get document
// get inexistent attachment
// remove document and conflict
o.rev = "3-6";
o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
"Remove document");
o.jio.remove({"_id": "doc1", "_rev": "2-5"}, o.f);
o.tick(o);
// remove document and conflict
o.rev = "3-7";
o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
"Remove document");
o.jio.remove({"_id": "doc1", "_rev": "2-3"}, o.f);
o.tick(o);
// remove document
o.rev = "2-8";
o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
"Remove document");
o.jio.remove({"_id": "doc1", "_rev": "1-1"}, o.f);
o.tick(o);
// get inexistent document
o.spy(o, "status", 404, "Get inexistent document");
o.jio.get({"_id": "doc1"}, {
"conflicts": true,
"revs": true,
"revs_info": true
}, o.f);
o.tick(o);
// // post a new document with id
// o.doc = {"_id": "doc1", "title": "post new doc with id"};
// o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
// "Post document (with id)");
// o.jio.post(o.doc, o.f);
// o.tick(o);
// // /
// // |
// // 1-1
// // check document
// o.local_rev_hash = generateRevisionHash(o.doc, o.revision);
// o.local_rev = "1-" + o.local_rev_hash;
// o.specific_rev_hash = o.local_rev_hash;
// o.specific_rev = o.local_rev;
// 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) +
// "/doc1" + suffix),
// doc, "Check document"
// );
// });
// // get the post document without revision
// o.spy(o, "value", {
// "_id": "doc1",
// "title": "post new doc with id",
// "_rev": "1-1",
// "_revisions": {"start": 1, "ids": ["1"]},
// "_revs_info": [{"rev": "1-1", "status": "available"}]
// }, "Get the previous document (without revision)");
// o.jio.get({"_id": "doc1"}, {
// "conflicts": true,
// "revs": true,
// "revs_info": true
// }, o.f);
// o.tick(o);
// // post same document without revision
// o.doc = {"_id": "doc1", "title": "post same document without revision"};
// o.rev = "1-2";
// o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
// "Post same document (without revision)");
// o.jio.post(o.doc, o.f);
// o.tick(o);
// // /
// // / \
// // 1-1 1-2
// // 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) +
// "/doc1" + suffix),
// doc, "Check document"
// );
// });
// // post a new revision
// o.doc = {"_id": "doc1", "title": "post new revision", "_rev": o.rev};
// o.rev = "2-3";
// o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
// "Post document (with revision)");
// o.jio.post(o.doc, o.f);
// o.tick(o);
// // /
// // / \
// // 1-1 1-2
// // |
// // 2-3
// // 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.specific_rev_conflict = o.local_rev;
// 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) +
// "/doc1" + suffix),
// doc, "Check document"
// );
// });
// // get the post document with revision
// o.spy(o, "value", {
// "_id": "doc1",
// "title": "post same document without revision",
// "_rev": "1-2",
// "_revisions": {"start": 1, "ids": ["2"]},
// "_revs_info": [{"rev": "1-2", "status": "available"}],
// "_conflicts": ["1-1"]
// }, "Get the previous document (with revision)");
// o.jio.get({"_id": "doc1", "_rev": "1-2"}, {
// "conflicts": true,
// "revs": true,
// "revs_info": true,
// }, o.f);
// o.tick(o);
// // get the post document with specific revision
// o.spy(o, "value", {
// "_id": "doc1",
// "title": "post new doc with id",
// "_rev": o.specific_rev,
// "_revisions": {"start": 1, "ids": [o.specific_rev_hash]},
// "_revs_info": [{"rev": o.specific_rev, "status": "available"}],
// "_conflicts": [o.specific_rev_conflict]
// }, "Get a previous document (with local storage revision)");
// o.jio.get({"_id": "doc1", "_rev": o.specific_rev}, {
// "conflicts": true,
// "revs": true,
// "revs_info": true,
// }, o.f);
// o.tick(o);
// // put document without id
// o.spy(o, "status", 20, "Put document without id")
// o.jio.put({}, o.f);
// o.tick(o);
// // put document without rev
// o.doc = {"_id": "doc1", "title": "put new document"};
// o.rev = "1-4";
// o.spy(o, "value", {"id": "doc1", "ok": true, "rev": o.rev},
// "Put document without rev")
// o.jio.put(o.doc, o.f);
// o.tick(o);
// // __/__
// // / | \
// // 1-1 1-2 1-4
// // |
// // 2-3
// // put new revision
// o.doc = {"_id": "doc1", "title": "put new revision", "_rev": "1-4"};
// o.rev = "2-5";
// o.spy(o, "value", {"id": "doc1", "ok": true, "rev": o.rev},
// "Put document without rev")
// o.jio.put(o.doc, o.f);
// o.tick(o);
// // __/__
// // / | \
// // 1-1 1-2 1-4
// // | |
// // 2-3 2-5
// // putAttachment to inexistent document
// // putAttachment
// // get document
// // get attachment
// // put document
// // get document
// // get attachment
// // remove attachment
// // get document
// // get inexistent attachment
// // remove document and conflict
// o.rev = "3-6";
// o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
// "Remove document");
// o.jio.remove({"_id": "doc1", "_rev": "2-5"}, o.f);
// o.tick(o);
// // remove document and conflict
// o.rev = "3-7";
// o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
// "Remove document");
// o.jio.remove({"_id": "doc1", "_rev": "2-3"}, o.f);
// o.tick(o);
// // remove document
// o.rev = "2-8";
// o.spy(o, "value", {"ok": true, "id": "doc1", "rev": o.rev},
// "Remove document");
// o.jio.remove({"_id": "doc1", "_rev": "1-1"}, o.f);
// o.tick(o);
// // get inexistent document
// o.spy(o, "status", 404, "Get inexistent document");
// o.jio.get({"_id": "doc1"}, {
// "conflicts": true,
// "revs": true,
// "revs_info": true
// }, o.f);
// o.tick(o);
o.jio.stop();
};
test ("[Revision + Local Storage] Scenario", function () {
testReplicateRevisionStorageGenerator(this, {
testReplicateRevisionStorage(this, {
"type": "replicaterevision",
"storage_list": [{
"type": "revision",
......@@ -2514,7 +2550,7 @@ module ("JIO Replicate Revision Storage");
});
});
test("[Replicate Revision + Revision + Local Storage] Scenario", function () {
testReplicateRevisionStorageGenerator(this, {
testReplicateRevisionStorage(this, {
"type": "replicaterevision",
"storage_list": [{
"type": "replicaterevision",
......@@ -2530,27 +2566,27 @@ module ("JIO Replicate Revision Storage");
});
});
test ("2x [Revision + Local Storage] Scenario", function () {
testReplicateRevisionStorageGenerator(this, {
testReplicateRevisionStorage(this, {
"type": "replicaterevision",
"storage_list": [{
"type": "revision",
"sub_storage": {
"type": "local",
"username": "ureprevlocloc1",
"application_name": "areprevloc1"
"application_name": "areprevlocloc1"
}
}, {
"type": "revision",
"sub_storage": {
"type": "local",
"username": "ureprevlocloc2",
"application_name": "areprevloc2"
"application_name": "areprevlocloc2"
}
}]
});
});
test("2x [Replicate Rev + 2x [Rev + Local]] Scenario", function () {
testReplicateRevisionStorageGenerator(this, {
testReplicateRevisionStorage(this, {
"type": "replicaterevision",
"storage_list": [{
"type": "replicaterevision",
......@@ -2589,6 +2625,159 @@ module ("JIO Replicate Revision Storage");
}]
});
});
test("Synchronisation", function () {
var o = generateTools(this);
o.jio = JIO.newJio({
"type": "replicaterevision",
"storage_list": [{
"type": "revision",
"sub_storage": {
"type": "local",
"username": "usyncreprevlocloc1",
"application_name": "asyncreprevlocloc1"
}
}, {
"type": "revision",
"sub_storage": {
"type": "local",
"username": "usyncreprevlocloc2",
"application_name": "asyncreprevlocloc2"
}
}]
});
o.localpath1 = "jio/localstorage/usyncreprevlocloc1/asyncreprevlocloc1";
o.localpath2 = "jio/localstorage/usyncreprevlocloc2/asyncreprevlocloc2";
// add documents to localstorage
o.doctree1_1 = {
"children": [{
"rev": "1-111",
"status": "available",
"children": [],
}]
};
o.doc1_1 = {"_id": "doc1.1-111", "title": "A"};
localstorage.setItem(o.localpath1 + "/doc1.revision_tree.json",
o.doctree1_1);
localstorage.setItem(o.localpath2 + "/doc1.revision_tree.json",
o.doctree1_1);
localstorage.setItem(o.localpath1 + "/" + o.doc1_1._id, o.doc1_1);
localstorage.setItem(o.localpath2 + "/" + o.doc1_1._id, o.doc1_1);
// no synchronisation
o.spy(o, "value", {"_id": "doc1", "_rev": "1-1", "title": "A"},
"Get document");
o.jio.get({"_id": "doc1"}, o.f);
o.tick(o);
// check documents from localstorage
deepEqual([
localstorage.getItem(o.localpath1 + "/doc1.revision_tree.json"),
localstorage.getItem(o.localpath2 + "/doc1.revision_tree.json"),
], [o.doctree1_1, o.doctree1_1], "Check revision trees, no synchro");
// add documents to localstorage
o.doctree2_2 = clone(o.doctree1_1);
o.doctree2_2.children[0].children.push({
"rev": "2-222",
"status": "available",
"children": []
});
o.doc2_2 = {"_id": "doc1.2-222", "title": "B"};
localstorage.setItem(o.localpath1 + "/doc1.revision_tree.json",
o.doctree2_2);
localstorage.setItem(o.localpath1 + "/" + o.doc2_2._id, o.doc2_2);
// document synchronisation without conflict
o.spy(o, "value", {"_id": "doc1", "_rev": "1-2", "title": "B"},
"Get document");
o.jio.get({"_id": "doc1"}, o.f);
o.tick(o, 50000);
// check documents from localstorage
deepEqual([
localstorage.getItem(o.localpath1 + "/doc1.revision_tree.json"),
localstorage.getItem(o.localpath2 + "/doc1.revision_tree.json"),
], [o.doctree2_2, o.doctree2_2], "Check revision trees, rev synchro");
// add documents to localstorage
o.doctree2_2.children[0].children.unshift({
"rev": "2-223",
"status": "available",
"children": []
});
o.doc2_2 = {"_id": "doc1.2-223", "title": "B"};
localstorage.setItem(o.localpath1 + "/doc1.revision_tree.json",
o.doctree2_2);
localstorage.setItem(o.localpath1 + "/" + o.doc2_2._id, o.doc2_2);
// document synchronisation with conflict
o.spy(o, "value", {"_id": "doc1", "_rev": "1-2", "title": "B"},
"Get document");
o.jio.get({"_id": "doc1"}, o.f);
o.tick(o, 50000);
// check documents from localstorage
deepEqual([
localstorage.getItem(o.localpath1 + "/doc1.revision_tree.json"),
localstorage.getItem(o.localpath2 + "/doc1.revision_tree.json"),
], [o.doctree2_2, o.doctree2_2], "Check revision trees, rev synchro");
////////////////////////////////////////////////////////////////////////////////
// // add documents to localstorage
// o.doctree2_2 = clone(o.doctree1_1);
// o.doctree2_2.children[0].children.push({
// "rev": "2-222",
// "status": "available",
// "children": []
// });
// o.doc2_2 = {"_id": "doc1.2-222", "title": "B"};
// localstorage.setItem(o.localpath1 + "/doc1.revision_tree.json",
// o.doctree2_2);
// localstorage.setItem(o.localpath1 + "/" + o.doc2_2._id, o.doc2_2);
// // document synchronisation without conflict
// o.spy(o, "value", {"_id": "doc1", "_rev": "1-2", "title": "B"},
// "Get document");
// o.jio.get({"_id": "doc1"}, o.f);
// o.tick(o, 50000);
// // check documents from localstorage
// deepEqual([
// localstorage.getItem(o.localpath1 + "/doc1.revision_tree.json"),
// localstorage.getItem(o.localpath2 + "/doc1.revision_tree.json"),
// ], [o.doctree2_2, o.doctree2_2], "Check revision trees, rev synchro");
// // add documents to localstorage
// o.doctree2_2.children[0].children.unshift({
// "rev": "2-223",
// "status": "available",
// "children": []
// });
// o.doc2_2 = {"_id": "doc1.2-223", "title": "B"};
// localstorage.setItem(o.localpath1 + "/doc1.revision_tree.json",
// o.doctree2_2);
// localstorage.setItem(o.localpath1 + "/" + o.doc2_2._id, o.doc2_2);
// // document synchronisation with conflict
// o.spy(o, "value", {"_id": "doc1", "_rev": "1-2", "title": "B"},
// "Get document");
// o.jio.get({"_id": "doc1"}, o.f);
// o.tick(o, 50000);
// // check documents from localstorage
// deepEqual([
// localstorage.getItem(o.localpath1 + "/doc1.revision_tree.json"),
// localstorage.getItem(o.localpath2 + "/doc1.revision_tree.json"),
// ], [o.doctree2_2, o.doctree2_2], "Check revision trees, rev synchro");
o.jio.stop();
});
/*
module ("Jio DAVStorage");
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment