diff --git a/Gruntfile.js b/Gruntfile.js index 980c17fd617bf30cf7266ba175571d90eeb3efc9..52a2816498acf444aaa64e46792eea5813727d60 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -182,7 +182,8 @@ module.exports = function (grunt) { 'src/jio.storage/indexeddbstorage.js', 'src/jio.storage/cryptstorage.js', 'src/jio.storage/websqlstorage.js', - 'src/jio.storage/fbstorage.js' + 'src/jio.storage/fbstorage.js', + 'src/jio.storage/revisionstorage.js' ], dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js' // dest: 'jio.js' diff --git a/examples/scenario_officejs.js b/examples/scenario_officejs.js new file mode 100644 index 0000000000000000000000000000000000000000..6a85d37cf2763e6a9963f4353b5170f93e8d6576 --- /dev/null +++ b/examples/scenario_officejs.js @@ -0,0 +1,680 @@ +/*global Blob*/ +/*jslint nomen: true, maxlen: 80*/ +(function (QUnit, jIO, Blob) { + "use strict"; + var test = QUnit.test, + // equal = QUnit.equal, + expect = QUnit.expect, + ok = QUnit.ok, + stop = QUnit.stop, + start = QUnit.start, + deepEqual = QUnit.deepEqual, + module = QUnit.module, + ATTACHMENT = 'data', + i, + name_list = ['get', 'post', 'put', 'buildQuery', + 'putAttachment', 'getAttachment', 'allAttachments']; + + /////////////////////////////////////////////////////// + // Fake Storage + /////////////////////////////////////////////////////// + function resetCount(count) { + for (i = 0; i < name_list.length; i += 1) { + count[name_list[i]] = 0; + } + } + + function MockStorage(spec) { + this._erp5_storage = jIO.createJIO({ + type: "erp5", + url: "http://example.org" + }); + this._sub_storage = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }); + this._options = spec.options; + resetCount(spec.options.count); + } + + function mockFunction(name) { + MockStorage.prototype[name] = function () { + this._options.count[name] += 1; + if (this._options.mock.hasOwnProperty(name)) { + return this._options.mock[name].apply(this, arguments); + } + return this._sub_storage[name].apply(this._sub_storage, arguments); + }; + } + + for (i = 0; i < name_list.length; i += 1) { + mockFunction(name_list[i]); + } + + MockStorage.prototype.hasCapacity = function (name) { + return this._erp5_storage.hasCapacity(name); + }; + + jIO.addStorage('mock', MockStorage); + + /////////////////////////////////////////////////////// + // Helpers + /////////////////////////////////////////////////////// + function putFullDoc(storage, id, doc, attachment) { + return storage.put(id, doc) + .push(function () { + return storage.putAttachment( + id, + ATTACHMENT, + attachment + ); + }); + } + + function equalStorage(storage, doc_tuple_list) { + return storage.allDocs() + .push(function (result) { + var i, + promise_list = []; + for (i = 0; i < result.data.rows.length; i += 1) { + promise_list.push(RSVP.all([ + result.data.rows[i].id, + storage.get(result.data.rows[i].id), + storage.getAttachment(result.data.rows[i].id, ATTACHMENT) + ])); + } + return RSVP.all(promise_list); + }) + .push(function (result) { + deepEqual(result, doc_tuple_list, 'Storage content'); + }); + } + + function isEmptyStorage(storage) { + return equalStorage(storage, []); + } + + function equalRemoteStorageCallCount(mock_count, expected_count) { + for (i = 0; i < name_list.length; i += 1) { + if (!expected_count.hasOwnProperty(name_list[i])) { + expected_count[name_list[i]] = 0; + } + } + deepEqual(mock_count, expected_count, 'Expected method call count'); + } + + /////////////////////////////////////////////////////// + // Module + /////////////////////////////////////////////////////// + module("scenario_officejs", { + setup: function () { + this.remote_mock_options = { + mock: { + remove: function () { + throw new Error('remove not supported'); + }, + removeAttachment: function () { + throw new Error('removeAttachment not supported'); + }, + allAttachments: function () { + return {data: null}; + }, + post: function (doc) { + var context = this; + return this._sub_storage.post(doc) + .push(function (post_id) { + context._options.last_post_id = post_id; + return post_id; + }); + } + }, + count: {} + }; + + this.jio = jIO.createJIO({ + type: "replicate", + query: { + query: 'portal_type:"Foo"', + sort_on: [["modification_date", "descending"]] + }, + signature_hash_key: 'modification_date', + use_remote_post: true, + conflict_handling: 1, + check_local_attachment_modification: true, + check_local_attachment_creation: true, + check_remote_attachment_modification: true, + check_remote_attachment_creation: true, + check_remote_attachment_deletion: true, + check_local_deletion: false, + parallel_operation_amount: 10, + parallel_operation_attachment_amount: 10, + local_sub_storage: { + type: "history", + sub_storage: { + type: "uuid", + sub_storage: { + type: "memory" + } + } + }, + signature_sub_storage: { + type: "query", + sub_storage: { + type: "memory" + } + }, + remote_sub_storage: { + type: "saferepair", + sub_storage: { + type: "mock", + options: this.remote_mock_options + } + } + }); + } + }); + + /////////////////////////////////////////////////////// + // Do nothing cases + /////////////////////////////////////////////////////// + test("empty: nothing to do", function () { + expect(2); + stop(); + + var test = this; + + this.jio.repair() + .then(function () { + return RSVP.all([ + isEmptyStorage(test.jio), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("allready synced: nothing to do", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + blob = new Blob(['a']); + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage(test.jio, [[doc_id, doc, blob]]), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Remote creation + /////////////////////////////////////////////////////// + test("remote document creation: copy", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + blob = new Blob(['a']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage(test.jio, [[doc_id, doc, blob]]), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1, get: 1, getAttachment: 1, allAttachments: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Remote modification + /////////////////////////////////////////////////////// + test("remote document modification: copy", function () { + expect(2); + stop(); + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + doc2 = {title: doc_id + 'a', portal_type: "Foo", modification_date: 'b'}, + blob = new Blob(['a']), + blob2 = new Blob(['b']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + return putFullDoc(test.jio.__storage._remote_sub_storage, doc_id, doc2, + blob2); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage(test.jio, [[doc_id, doc2, blob2]]), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1, get: 1, getAttachment: 1, allAttachments: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Remote hide + /////////////////////////////////////////////////////// + test("remote document deletion: delete", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + blob = new Blob(['a']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + test.remote_mock_options.mock.buildQuery = function () { + return []; + }; + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + isEmptyStorage(test.jio), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Local creation + /////////////////////////////////////////////////////// + test("local document creation: copy", function () { + expect(3); + stop(); + + var test = this, + doc_id = 'abc', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + blob = new Blob(['a']); + putFullDoc(this.jio, doc_id, doc, blob) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage( + test.jio, + [[test.remote_mock_options.last_post_id, doc, blob]] + ), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1, post: 1, putAttachment: 1, allAttachments: 1} + ) + ]); + }) + .then(function () { + return equalStorage( + test.jio.__storage._remote_sub_storage, + [[test.remote_mock_options.last_post_id, doc, blob]] + ); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Local modification + /////////////////////////////////////////////////////// + test("local document modification: copy", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + doc2 = {title: doc_id + 'a', portal_type: "Foo", modification_date: 'b'}, + blob = new Blob(['a']), + blob2 = new Blob(['b']), + last_id; + + putFullDoc(this.jio, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + last_id = test.remote_mock_options.last_post_id; + return putFullDoc(test.jio, last_id, doc2, blob2); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage( + test.jio.__storage._remote_sub_storage, + [[last_id, doc2, blob2]] + ), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1, put: 1, + allAttachments: 1, putAttachment: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Conflict + /////////////////////////////////////////////////////// + test("both modification: keep local", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + doc2 = {title: doc_id + 'a', portal_type: "Foo", modification_date: 'b'}, + doc3 = {title: doc_id + 'c', portal_type: "Foo", modification_date: 'c'}, + blob = new Blob(['a']), + blob2 = new Blob(['b']), + blob3 = new Blob(['c']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + putFullDoc(test.jio.__storage._remote_sub_storage, doc_id, + doc2, blob2), + putFullDoc(test.jio, doc_id, doc3, blob3) + ]); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage( + test.jio.__storage._remote_sub_storage, + [[doc_id, doc3, blob3]] + ), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1, put: 1, + allAttachments: 1, putAttachment: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("local modification / frozen remote", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + doc2 = {title: doc_id + 'a', portal_type: "Foo", modification_date: 'b'}, + blob = new Blob(['a']), + blob2 = new Blob(['b']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + return putFullDoc(test.jio, doc_id, doc2, blob2); + }) + .then(function () { + test.remote_mock_options.mock.put = function (id) { + if (id === doc_id) { + throw new jIO.util.jIOError('put not allowed', 403); + } + return id; + }; + test.remote_mock_options.mock.putAttachment = function (id) { + if (id === doc_id) { + throw new jIO.util.jIOError('putattachment not allowed', 403); + } + return id; + }; + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + ok(false, 'notimplemented'); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + /////////////////////////////////////////////////////// + // Local deletion (aka, people playing manually with the browser storage) + /////////////////////////////////////////////////////// + test("local document deletion: do nothing", function () { + expect(3); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + blob = new Blob(['a']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + return test.jio.remove(doc_id); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + isEmptyStorage(test.jio), + equalStorage( + test.jio.__storage._remote_sub_storage, + [[doc_id, doc, blob]] + ), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("local attachment deletion: do nothing", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + blob = new Blob(['a']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + return test.jio.removeAttachment(doc_id, ATTACHMENT); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage( + test.jio.__storage._remote_sub_storage, + [[doc_id, doc, blob]] + ), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + + test("local deletion / remote modification", function () { + expect(2); + stop(); + + var test = this, + doc_id = 'foo_module/1', + doc = {title: doc_id, portal_type: "Foo", modification_date: 'a'}, + doc2 = {title: doc_id + 'a', portal_type: "Foo", modification_date: 'b'}, + blob = new Blob(['a']), + blob2 = new Blob(['b']); + + putFullDoc(this.jio.__storage._remote_sub_storage, doc_id, doc, blob) + .then(function () { + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + putFullDoc(test.jio.__storage._remote_sub_storage, doc_id, + doc2, blob2), + test.jio.remove(doc_id) + ]); + }) + .then(function () { + resetCount(test.remote_mock_options.count); + return test.jio.repair(); + }) + .then(function () { + return RSVP.all([ + equalStorage( + test.jio, + [[doc_id, doc2, blob2]] + ), + equalStorage( + test.jio.__storage._remote_sub_storage, + [[doc_id, doc2, blob2]] + ), + equalRemoteStorageCallCount( + test.remote_mock_options.count, + {buildQuery: 1, get: 1, + allAttachments: 1, getAttachment: 1} + ) + ]); + }) + .fail(function (error) { + ok(false, error); + }) + .always(function () { + start(); + }); + }); + +}(QUnit, jIO, Blob)); diff --git a/src/jio.storage/revisionstorage.js b/src/jio.storage/revisionstorage.js index 2e85aa8bf67c0788b3daacbf0ad5453062b43f5d..286e3e2449ad44a73e94328f3d2e6c8a354fb4ff 100644 --- a/src/jio.storage/revisionstorage.js +++ b/src/jio.storage/revisionstorage.js @@ -1,1064 +1,619 @@ -/*jslint indent: 2, maxlen: 80, nomen: true */ -/*global jIO, hex_sha256, define */ - -/** - * JIO Revision Storage. - * It manages document version and can generate conflicts. - * Description: - * { - * "type": "revision", - * "sub_storage": - * } - */ -// define([module_name], [dependencies], module); -(function (dependencies, module) { - "use strict"; - if (typeof define === 'function' && define.amd) { - return define(dependencies, module); - } - module(jIO, {hex_sha256: hex_sha256}); -}(['jio', 'sha256'], function (jIO, sha256) { +/*jslint nomen: true*/ +/*global RSVP, SimpleQuery, ComplexQuery*/ +(function (jIO, RSVP, SimpleQuery, ComplexQuery) { "use strict"; - var tool = { - "readBlobAsBinaryString": jIO.util.readBlobAsBinaryString, - "uniqueJSONStringify": jIO.util.uniqueJSONStringify - }; - - jIO.addStorage("revision", function (spec) { - - var that = this, priv = {}; - spec = spec || {}; - // ATTRIBUTES // - priv.doc_tree_suffix = ".revision_tree.json"; - priv.sub_storage = spec.sub_storage; - // METHODS // - /** - * Clones an object in deep (without functions) - * @method clone - * @param {any} object The object to clone - * @return {any} The cloned object - */ - priv.clone = function (object) { - var tmp = JSON.stringify(object); - if (tmp === undefined) { - return undefined; - } - return JSON.parse(tmp); - }; + // Used to distinguish between operations done within the same millisecond + function generateUniqueTimestamp(time) { - /** - * Generate a new uuid - * @method generateUuid - * @return {string} The new uuid - */ - priv.generateUuid = function () { - var S4 = function () { - /* 65536 */ - var i, string = Math.floor( - Math.random() * 0x10000 - ).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 - * @param {string} string The string to hash - * @return {string} The string hash code - */ - priv.hashCode = function (string) { - return sha256.hex_sha256(string); - }; - - /** - * Checks a revision format - * @method checkDocumentRevisionFormat - * @param {object} doc The document object - * @return {object} null if ok, else error object - */ - priv.checkDocumentRevisionFormat = function (doc) { - var send_error = function (message) { - return { - "status": 409, - "message": message, - "reason": "Wrong revision" - }; - }; - 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"); - } - } - }; + // XXX: replace this with UUIDStorage function call to S4() when it becomes + // publicly accessible + var uuid = ('0000' + Math.floor(Math.random() * 0x10000) + .toString(16)).slice(-4), + //timestamp = Date.now().toString(); + timestamp = time.toString(); + return timestamp + "-" + uuid; + } - /** - * Creates a new document tree - * @method newDocTree - * @return {object} The new document tree - */ - priv.newDocTree = function () { - return {"children": []}; - }; + function isTimestamp(id) { + //A timestamp is of the form + //"[13 digit number]-[4 numbers/lowercase letters]" + var re = /^[0-9]{13}-[a-z0-9]{4}$/; + return re.test(id); + } - /** - * 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 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]); + function removeOldRevs( + substorage, + results, + keepDoc + ) { + var ind, + promises = [], + seen = {}, + docum, + log, + start_ind, + new_promises, + doc_id, + checkIsId, + removeDoc; + for (ind = 0; ind < results.data.rows.length; ind += 1) { + docum = results.data.rows[ind]; + // Count the number of revisions of each document, and delete older + // ones. + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {count: 0}; } - return revisions; - }; + log = seen[docum.value.doc_id]; + log.count += 1; + //log.id = docum.id; - /** - * Convert the revision history object to an array of revisions. - * @method revisionHistoryToList - * @param {object} revs The revision history - * @return {array} The revision array - */ - 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]); + // Record the index of the most recent edit that is before the cutoff + if (!log.hasOwnProperty("s") && !keepDoc({doc: docum, log: log})) { + log.s = ind; } - return new_list; - }; - /** - * 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.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"}); + // Record the index of the most recent put or remove + if ((!log.hasOwnProperty("pr")) && + (docum.value.op === "put" || docum.value.op === "remove")) { + log.pr = ind; + log.final = ind; } - 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; - }; - /** - * Update a document metadata revision properties - * @method fillDocumentRevisionProperties - * @param {object} doc The document object - * @param {object} doc_tree The document tree - */ - 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 ((docum.op === "putAttachment" || docum.op === "removeAttachment") && + log.hasOwnProperty(docum.name) && + !log[docum.name].hasOwnProperty("prA")) { + log[docum.name].prA = ind; + log.final = ind; } - if (doc._revs.start > 0) { - doc._rev = doc._revs.start + "-" + doc._revs.ids[0]; - } else { - delete doc._rev; + } + checkIsId = function (d) { + return d.value.doc_id === doc_id; + }; + removeDoc = function (d) { + return substorage.remove(d.id); + }; + for (doc_id in seen) { + if (seen.hasOwnProperty(doc_id)) { + log = seen[doc_id]; + start_ind = Math.max(log.s, log.final + 1); + new_promises = results.data.rows + .slice(start_ind) + .filter(checkIsId) + .map(removeDoc); + promises = promises.concat(new_promises); } - }; + } + return RSVP.all(promises); + } - /** - * 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.generateNextRevision = function (doc, deleted_flag) { - var string, revision_history, revs_info; - doc = priv.clone(doc) || {}; - revision_history = doc._revs; - revs_info = doc._revs_info; - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - string = tool.uniqueJSONStringify(doc) + - tool.uniqueJSONStringify(revision_history) + - JSON.stringify(deleted_flag ? true : false); - 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; - }; + function throwCantFindError(id) { + throw new jIO.util.jIOError( + "RevisionStorage: cannot find object '" + id + "'", + 404 + ); + } - /** - * 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}]; - } - revs_info = getRevisionInfoRec(child); - if (revs_info.length > 0 || revision === undefined) { - revs_info.push({"rev": child.rev, "status": child.status}); - return revs_info; - } - } - return []; - }; - return getRevisionInfoRec(doc_tree); - }; + function throwRemovedError(id) { + throw new jIO.util.jIOError( + "RevisionStorage: cannot find object '" + id + "' (removed)", + 404 + ); + } - priv.updateDocumentTree = function (doc, doc_tree) { - var revs_info, updateDocumentTreeRec; - 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; - } - 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": [] + /** + * The jIO RevisionStorage extension + * + * @class RevisionStorage + * @constructor + */ + function RevisionStorage(spec) { + this._sub_storage = jIO.createJIO(spec.sub_storage); + if (spec.hasOwnProperty("include_revisions")) { + this._include_revisions = spec.include_revisions; + } else { + this._include_revisions = false; + } + var substorage = this._sub_storage; + this.packOldRevisions = function (save_info) { + /** + save_info has this form: + { + keep_latest_num: 10, + keep_active_revs: timestamp + } + keep_latest_num = x: keep at most the x latest copies of each unique doc + keep_active_revs = x: throw away all outdated revisions from before x + **/ + var options = { + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "op"] + }, + keep_fixed_num = save_info.hasOwnProperty("keep_latest_num"); + return substorage.allDocs(options) + .push(function (results) { + if (keep_fixed_num) { + return removeOldRevs(substorage, results, function (data) { + return data.log.count <= save_info.keep_latest_num; + }); + } + return removeOldRevs(substorage, results, function (data) { + return data.doc.id > save_info.keep_active_revs; + }); }); - updateDocumentTreeRec(doc_tree.children[0], revs_info); - }; - updateDocumentTreeRec(doc_tree, priv.clone(revs_info)); }; + } - priv.send = function (command, method, doc, option, callback) { - var storage = command.storage(priv.sub_storage); - function onSuccess(success) { - callback(undefined, success); - } - function onError(err) { - callback(err, undefined); - } - if (method === 'allDocs') { - storage.allDocs(option).then(onSuccess, onError); - } else { - storage[method](doc, option).then(onSuccess, onError); - } - }; + RevisionStorage.prototype.get = function (id_in) { - priv.getWinnerRevsInfo = function (doc_tree) { - var revs_info = [], getWinnerRevsInfoRec; - getWinnerRevsInfoRec = function (doc_tree, tmp_revs_info) { - var i; - if (doc_tree.rev) { - tmp_revs_info.unshift({ - "rev": doc_tree.rev, - "status": doc_tree.status - }); - } - if (doc_tree.children.length === 0) { - if (revs_info.length === 0 || - (revs_info[0].status !== "available" && - tmp_revs_info[0].status === "available") || - (tmp_revs_info[0].status === "available" && - revs_info.length < tmp_revs_info.length)) { - revs_info = priv.clone(tmp_revs_info); - } - } - for (i = 0; i < doc_tree.children.length; i += 1) { - getWinnerRevsInfoRec(doc_tree.children[i], tmp_revs_info); - } - tmp_revs_info.shift(); - }; - getWinnerRevsInfoRec(doc_tree, []); - return revs_info; - }; + // Query to get the last edit made to this document + var substorage = this._sub_storage, + doc_id_query, + metadata_query, + options; - priv.getConflicts = function (revision, doc_tree) { - var conflicts = [], getConflictsRec; - getConflictsRec = function (doc_tree) { - var i; - if (doc_tree.rev === revision) { - return; - } - if (doc_tree.children.length === 0) { - if (doc_tree.status !== "deleted") { - conflicts.push(doc_tree.rev); - } - } - for (i = 0; i < doc_tree.children.length; i += 1) { - getConflictsRec(doc_tree.children[i]); - } + if (this._include_revisions) { + doc_id_query = new SimpleQuery({ + operator: "<=", + key: "timestamp", + value: id_in + }); + } else { + doc_id_query = new SimpleQuery({key: "doc_id", value: id_in}); + } + + // Include id_in as value in query object for safety + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + doc_id_query, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new SimpleQuery({key: "op", value: "put"}) + ] + }) + ] + }); + options = { + query: metadata_query, + select_list: ["op"], + limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + + + return substorage.allDocs(options) + .push(function (results) { + if (results.data.total_rows > 0) { + if (results.data.rows[0].value.op === "put") { + return substorage.get(results.data.rows[0].id) + .push(function (result) { + return result.doc; + }); + } + throwRemovedError(id_in); + } + throwCantFindError(id_in); + }); + }; + + RevisionStorage.prototype.put = function (id, data) { + var substorage = this._sub_storage, + timestamp = generateUniqueTimestamp(Date.now()), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + doc: data, + op: "put" }; - getConflictsRec(doc_tree); - return conflicts.length === 0 ? undefined : conflicts; - }; - priv.get = function (command, doc, option, callback) { - priv.send(command, "get", doc, option, callback); - }; - priv.put = function (command, doc, option, callback) { - priv.send(command, "put", doc, option, callback); - }; - priv.remove = function (command, doc, option, callback) { - priv.send(command, "remove", doc, option, callback); - }; - priv.getAttachment = function (command, attachment, option, callback) { - priv.send(command, "getAttachment", attachment, option, callback); - }; - priv.putAttachment = function (command, attachment, option, callback) { - priv.send(command, "putAttachment", attachment, option, callback); - }; - priv.removeAttachment = function (command, attachment, option, callback) { - priv.send(command, "removeAttachment", attachment, option, callback); - }; + if (this._include_revisions && isTimestamp(id)) { + return substorage.get(id) + .push(function (metadata) { + metadata.timestamp = timestamp; + metadata.doc = data; + return substorage.put(timestamp, metadata); + }, + function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + return substorage.put(timestamp, metadata); + } + throw error; + }); + } + return this._sub_storage.put(timestamp, metadata); + }; - priv.getDocument = function (command, 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(command, doc, option, callback); - }; - priv.putDocument = function (command, doc, option, callback) { - doc = priv.clone(doc); - doc._id = doc._id + "." + doc._rev; - delete doc._attachment; - delete doc._data; - delete doc._mimetype; - delete doc._content_type; - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - priv.put(command, doc, option, callback); - }; + RevisionStorage.prototype.remove = function (id) { + var timestamp = generateUniqueTimestamp(Date.now() - 1), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + op: "remove" + }; + return this._sub_storage.put(timestamp, metadata); + }; - priv.getRevisionTree = function (command, doc, option, callback) { - doc = priv.clone(doc); - doc._id = doc._id + priv.doc_tree_suffix; - priv.get(command, doc, option, function (err, response) { - if (err) { - return callback(err, response); - } - if (response.data && response.data.children) { - response.data.children = JSON.parse(response.data.children); + RevisionStorage.prototype.allAttachments = function (id) { + var substorage = this._sub_storage, + query_obj, + query_removed_check, + options, + query_doc_id, + options_remcheck, + include_revs = this._include_revisions, + have_seen_id = false; + + // id is a timestamp, and allAttachments will return attachment versions + // up-to-and-including those made at time id + if (include_revs) { + query_doc_id = new SimpleQuery({ + operator: "<=", + key: "timestamp", + value: id + }); + } else { + query_doc_id = new SimpleQuery({key: "doc_id", value: id}); + have_seen_id = true; + } + + query_removed_check = new ComplexQuery({ + operator: "AND", + query_list: [ + query_doc_id, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "put"}), + new SimpleQuery({key: "op", value: "remove"}) + ] + }) + ] + }); + + query_obj = new ComplexQuery({ + operator: "AND", + query_list: [ + query_doc_id, + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }) + ] + }); + + options_remcheck = { + query: query_removed_check, + select_list: ["op", "timestamp"], + limit: [0, 1], + sort_on: [["timestamp", "descending"]] + }; + options = { + query: query_obj, + sort_on: [["timestamp", "descending"]], + select_list: ["op", "name"] + }; + + return this._sub_storage.allDocs(options_remcheck) + // Check the document exists and is not removed + .push(function (results) { + if (results.data.total_rows > 0) { + if (results.data.rows[0].id === id) { + have_seen_id = true; + } + if (results.data.rows[0].value.op === "remove") { + throwRemovedError(id); + } + } else { + throwCantFindError(id); + } + }) + .push(function () { + return substorage.allDocs(options); + }) + .push(function (results) { + var seen = {}, + attachments = [], + attachment_promises = [], + ind, + entry; + + // If input mapped to a real timestamp, then the first query result must + // have the inputted id. Otherwise, unexpected results could arise + // by inputting nonsensical strings as id when include_revisions = true + if (include_revs && + results.data.total_rows > 0 && + results.data.rows[0].id !== id && + !have_seen_id) { + throwCantFindError(id); + } + + + // Only return attachments if: + // (it is the most recent revision) AND (it is a putAttachment) + attachments = results.data.rows.filter(function (docum) { + if (!seen.hasOwnProperty(docum.value.name)) { + var output = (docum.value.op === "putAttachment"); + seen[docum.value.name] = {}; + return output; + } + }); + // Assembles object of attachment_name: attachment_object + for (ind = 0; ind < attachments.length; ind += 1) { + entry = attachments[ind]; + attachment_promises[entry.value.name] = + substorage.getAttachment(entry.id, entry.value.name); } - return callback(err, response); + return RSVP.hash(attachment_promises); }); - }; + }; - priv.getAttachmentList = function (command, doc, option, callback) { - var attachment_id, dealResults, state = "ok", result_list = [], count = 0; - dealResults = function (attachment_id, attachment_meta) { - return function (err, response) { - if (state !== "ok") { - return; - } - count -= 1; - if (err) { - if (err.status === 404) { - result_list.push(undefined); - } else { - state = "error"; - return callback(err, undefined); + RevisionStorage.prototype.putAttachment = function (id, name, blob) { + var timestamp = generateUniqueTimestamp(Date.now()), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + name: name, + op: "putAttachment" + }, + substorage = this._sub_storage; + + if (this._include_revisions && isTimestamp(id)) { + return substorage.get(id) + .push(function (metadata) { + metadata.timestamp = timestamp; + metadata.name = name; + }, + function (error) { + if (!(error.status_code === 404 && + error instanceof jIO.util.jIOError)) { + throw error; } - } - result_list.push({ - "_attachment": attachment_id, - "_data": response.data, - "_content_type": attachment_meta.content_type }); - if (count === 0) { - state = "finished"; - callback(undefined, {"data": result_list}); - } - }; + } + return this._sub_storage.put(timestamp, metadata) + .push(function () { + return substorage.putAttachment(timestamp, name, blob); + }); + }; + RevisionStorage.prototype.getAttachment = function (id, name) { + + // In this case, id is a timestamp, so return attachment version at that + // time + if (this._include_revisions) { + return this._sub_storage.getAttachment(id, name) + .push(undefined, function (error) { + if (error.status_code === 404 && + error instanceof jIO.util.jIOError) { + throwCantFindError(id); + } + throw error; + }); + } + + // Query to get the last edit made to this document + var substorage = this._sub_storage, + + // "doc_id: id AND + // (op: remove OR ((op: putAttachment OR op: removeAttachment) AND + // name: name))" + metadata_query = new ComplexQuery({ + operator: "AND", + query_list: [ + new SimpleQuery({key: "doc_id", value: id}), + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "remove"}), + new ComplexQuery({ + operator: "AND", + query_list: [ + new ComplexQuery({ + operator: "OR", + query_list: [ + new SimpleQuery({key: "op", value: "putAttachment"}), + new SimpleQuery({key: "op", value: "removeAttachment"}) + ] + }), + new SimpleQuery({key: "name", value: name}) + ] + }) + ] + }) + ] + }), + options = { + query: metadata_query, + sort_on: [["timestamp", "descending"]], + limit: [0, 1], + select_list: ["op", "name"] }; - for (attachment_id in doc._attachments) { - if (doc._attachments.hasOwnProperty(attachment_id)) { - count += 1; - priv.getAttachment( - command, - {"_id": doc._id, "_attachment": attachment_id}, - option, - dealResults(attachment_id, doc._attachments[attachment_id]) - ); - } - } - if (count === 0) { - callback(undefined, {"data": []}); - } - }; - - priv.putAttachmentList = function (command, doc, option, - attachment_list, callback) { - var i, dealResults, state = "ok", count = 0, attachment; - attachment_list = attachment_list || []; - dealResults = function () { - return function (err) { - if (state !== "ok") { - return; - } - count -= 1; - if (err) { - state = "error"; - return callback(err, undefined); - } - if (count === 0) { - state = "finished"; - callback(undefined, {}); + return substorage.allDocs(options) + .push(function (results) { + if (results.data.total_rows > 0) { + // XXX: issue if attachments are put on a removed document + if (results.data.rows[0].value.op === "remove" || + results.data.rows[0].value.op === "removeAttachment") { + throwRemovedError(id); } - }; - }; - for (i = 0; i < attachment_list.length; i += 1) { - attachment = attachment_list[i]; - if (attachment !== undefined) { - count += 1; - attachment._id = doc._id + "." + doc._rev; - priv.putAttachment(command, attachment, option, dealResults(i)); + return substorage.getAttachment(results.data.rows[0].id, name); } - } - if (count === 0) { - return callback(undefined, {}); - } - }; - - priv.putDocumentTree = function (command, doc, option, doc_tree, callback) { - doc_tree = priv.clone(doc_tree); - doc_tree._id = doc._id + priv.doc_tree_suffix; - if (doc_tree.children) { - doc_tree.children = JSON.stringify(doc_tree.children); - } - priv.put(command, doc_tree, option, callback); - }; + throwCantFindError(id); + }); + }; - priv.notFoundError = function (message, reason) { - return { - "status": 404, - "message": message, - "reason": reason + RevisionStorage.prototype.removeAttachment = function (id, name) { + var timestamp = generateUniqueTimestamp(Date.now()), + metadata = { + // XXX: remove this attribute once query can sort_on id + timestamp: timestamp, + doc_id: id, + name: name, + op: "removeAttachment" }; - }; + return this._sub_storage.put(timestamp, metadata); + }; - priv.conflictError = function (message, reason) { - return { - "status": 409, - "message": message, - "reason": reason - }; - }; + RevisionStorage.prototype.repair = function () { + return this._sub_storage.repair.apply(this._sub_storage, arguments); + }; - priv.revisionGenericRequest = function (command, 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 (specific_parameter.attachment_id) { - doc._attachment = specific_parameter.attachment_id; - } - callback.begin = function () { - var check_error; - doc._id = doc._id || priv.generateUuid(); // XXX should not generate id - 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(command, doc, option, callback.getRevisionTree); - }; - callback.getRevisionTree = function (err, response) { - var winner_info, previous_revision, generate_new_revision; - 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); - } - } - doc_tree = (response && response.data) || priv.newDocTree(); - if (specific_parameter.get || specific_parameter.getAttachment) { - if (!doc._rev) { - winner_info = priv.getWinnerRevsInfo(doc_tree); - if (winner_info.length === 0) { - return onEnd(priv.notFoundError( - "Document not found", - "missing" - ), undefined); - } - 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(command, 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 - ); - } - if (doc._revs_info.length > 1) { - prev_doc = { - "_id": doc._id, - "_rev": doc._revs_info[1].rev - }; - if (!generate_new_revision && specific_parameter.putAttachment) { - prev_doc._rev = doc._revs_info[0].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(command, prev_doc, - option, callback.getDocument); - } - if (specific_parameter.remove || specific_parameter.removeAttachment) { - return onEnd(priv.notFoundError( - "Unable to remove an inexistent document", - "missing" - ), undefined); - } - priv.putDocument(command, doc, option, callback.putDocument); - }; - callback.getDocument = function (err, res_doc) { - 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); - } - if (specific_parameter.get) { - return onEnd(priv.notFoundError( - "Unable to find the document", - "missing" - ), undefined); + RevisionStorage.prototype.hasCapacity = function (name) { + return name === 'list' || + name === 'include' || + name === 'query' || + name === 'select'; + }; + + RevisionStorage.prototype.buildQuery = function (options) { + // Set default values + if (options === undefined) {options = {}; } + if (options.query === undefined) {options.query = ""; } + if (options.sort_on === undefined) {options.sort_on = []; } + if (options.select_list === undefined) {options.select_list = []; } + options.query = jIO.QueryFactory.create(options.query); + var meta_options = { + query: "", + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "op", "doc_id", "timestamp"] + }, + include_revs = this._include_revisions; + + if (include_revs) {// && options.query.key === "doc_id") { + meta_options.query = options.query; + } + + return this._sub_storage.allDocs(meta_options) + .push(function (results) { + results = results.data.rows; + var seen = {}, + docs_to_query, + i; + + if (include_revs) { + + // We only query on versions mapping to puts or putAttachments + results = results.map(function (docum, ind) { + var data_key; + if (docum.value.op === "put") { + return docum; } - res_doc = {"data": {}}; - } else { - err.message = "Cannot get document"; - return onEnd(err, undefined); - } - } - res_doc = res_doc.data; - 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; + if (docum.value.op === "remove") { + docum.value.doc = {}; + return docum; } - } - if (option.revs === true) { - res_doc._revisions = doc._revs; - } - if (option.revs_info === true) { - res_doc._revs_info = doc._revs_info; - } - return onEnd(undefined, {"data": res_doc}); - } - if (specific_parameter.putAttachment || - 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]; + if (docum.value.op === "putAttachment" || + docum.value.op === "removeAttachment") { + + // putAttachment document does not contain doc metadata, so we + // add it from the most recent non-removed put on same id + docum.value.doc = {}; + for (i = ind + 1; i < results.length; i += 1) { + if (results[i].value.doc_id === docum.value.doc_id) { + if (results[i].value.op === "put") { + for (data_key in results[i].value.doc) { + if (results[i].value.doc.hasOwnProperty(data_key)) { + docum.value.doc[data_key] = + results[i].value.doc[data_key]; + } + } + return docum; + } + // If most recent metadata edit before the attachment edit + // was a remove, then leave doc empty + if (results[i].value.op === "remove") { + return docum; + } + } + } } - } - } - if (specific_parameter.remove) { - priv.putDocumentTree(command, doc, option, - doc_tree, callback.putDocumentTree); + return false; + }); } else { - priv.getAttachmentList(command, res_doc, option, - callback.getAttachmentList); - } - }; - callback.getAttachmentList = function (err, res_list) { - var i, attachment_found = false; - if (err) { - err.message = "Cannot get attachment"; - return onEnd(err, undefined); - } - res_list = res_list.data; - 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(undefined, {"data": attachment_list[i]._data}); - } - } - 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); - } - } - priv.putDocument(command, doc, option, callback.putDocument); - }; - callback.putDocument = function (err) { - 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 (attachment_list[i] && - specific_parameter.add_to_attachment_list._attachment === - attachment_list[i]._attachment) { - attachment_found = true; - attachment_list[i] = specific_parameter.add_to_attachment_list; - break; - } - } - if (!attachment_found) { - attachment_list.unshift(specific_parameter.add_to_attachment_list); - } - } - priv.putAttachmentList( - command, - doc, - option, - attachment_list, - callback.putAttachmentList - ); - }; - callback.putAttachmentList = function (err) { - if (err) { - err.message = "Cannot copy attacments to the document"; - return onEnd(err, undefined); - } - priv.putDocumentTree(command, doc, option, - doc_tree, callback.putDocumentTree); - }; - callback.putDocumentTree = function (err) { - var response_object; - if (err) { - err.message = "Cannot update the document history"; - return onEnd(err, undefined); - } - response_object = { - "id": doc._id, - "rev": doc._rev - }; - if (specific_parameter.putAttachment || - specific_parameter.removeAttachment || - specific_parameter.getAttachment) { - response_object.attachment = doc._attachment; - } - onEnd(undefined, response_object); - // if (option.keep_revision_history !== true) { - // // priv.remove(command, prev_doc, option, function () { - // // - change "available" status to "deleted" - // // - remove attachments - // // - done, no callback - // // }); - // } - }; - callback.begin(); - }; - - /** - * 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, metadata, option) { - priv.revisionGenericRequest( - command, - metadata, - option, - {}, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"id": response.id, "rev": response.rev}); - } - ); - }; - - /** - * Put 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 put - * @param {object} command The JIO command - */ - that.put = function (command, metadata, option) { - priv.revisionGenericRequest( - command, - metadata, - option, - {}, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"rev": response.rev}); - } - ); - }; + // Only query on latest revisions of non-removed documents/attachment + // edits + results = results.map(function (docum, ind) { + var data_key; + if (docum.value.op === "put") { + // Mark as read and include in query + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {}; + return docum; + } - that.putAttachment = function (command, param, option) { - tool.readBlobAsBinaryString(param._blob).then(function (event) { - param._content_type = param._blob.type; - param._data = event.target.result; - delete param._blob; - priv.revisionGenericRequest( - command, - param, - option, - { - "doc_id": param._id, - "attachment_id": param._attachment, - "add_to_attachment_list": { - "_attachment": param._attachment, - "_content_type": param._content_type, - "_data": param._data - }, - "putAttachment": true - }, - function (err, response) { - if (err) { - return command.error(err); + } else if (docum.value.op === "remove" || + docum.value.op === "removeAttachment") { + // Mark as read but do not include in query + seen[docum.value.doc_id] = {}; + + } else if (docum.value.op === "putAttachment") { + // If latest edit, mark as read, add document metadata from most + // recent put, and add to query + if (!seen.hasOwnProperty(docum.value.doc_id)) { + seen[docum.value.doc_id] = {}; + docum.value.doc = {}; + for (i = ind + 1; i < results.length; i += 1) { + if (results[i].value.doc_id === docum.value.doc_id) { + if (results[i].value.op === "put") { + for (data_key in results[i].value.doc) { + if (results[i].value.doc.hasOwnProperty(data_key)) { + docum.value.doc[data_key] = + results[i].value.doc[data_key]; + } + } + return docum; + } + if (results[i].value.op === "remove") { + // If most recent edit on document was a remove before + // this attachment, then don't include attachment in query + return false; + } + docum.value.doc = {}; + } + } + } } - command.success({"rev": response.rev}); - } - ); - }, function () { - command.error("conflict", "broken blob", "Cannot read data to put"); - }); - }; - - that.remove = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "revision_needed": true, - "remove": true - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"rev": response.rev}); + return false; + }); } - ); - }; + docs_to_query = results - that.removeAttachment = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "doc_id": param._id, - "attachment_id": param._attachment, - "revision_needed": true, - "removeAttachment": true, - "remove_from_attachment_list": { - "_attachment": param._attachment - } - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"rev": response.rev}); - } - ); - }; + // Filter out all docs flagged as false in previous map call + .filter(function (docum) { + return docum; + }) - that.get = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "get": true - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"data": response.data}); - } - ); - }; + // Put into correct format to be passed back to query storage + .map(function (docum) { - that.getAttachment = function (command, param, option) { - priv.revisionGenericRequest( - command, - param, - option, - { - "doc_id": param._id, - "attachment_id": param._attachment, - "getAttachment": true - }, - function (err, response) { - if (err) { - return command.error(err); - } - command.success({"data": response.data}); - } - ); - }; - - that.allDocs = function (command, param, option) { - /*jslint unparam: true */ - var rows, result = {"total_rows": 0, "rows": []}, functions = {}; - functions.finished = 0; - functions.falseResponseGenerator = function (response, callback) { - callback(undefined, response); - }; - functions.fillResultGenerator = function (doc_id) { - return function (err, doc_tree) { - var document_revision, row, revs_info; - if (err) { - return command.error(err); - } - doc_tree = doc_tree.data; - if (typeof doc_tree.children === 'string') { - doc_tree.children = JSON.parse(doc_tree.children); - } - revs_info = priv.getWinnerRevsInfo(doc_tree); - document_revision = - rows.document_revisions[doc_id + "." + revs_info[0].rev]; - if (document_revision) { - row = { - "id": doc_id, - "key": doc_id, - "value": { - "rev": revs_info[0].rev - } - }; - if (document_revision.doc && option.include_docs) { - document_revision.doc._id = doc_id; - document_revision.doc._rev = revs_info[0].rev; - row.doc = document_revision.doc; - } - result.rows.push(row); - result.total_rows += 1; - } - functions.success(); - }; - }; - functions.success = function () { - functions.finished -= 1; - if (functions.finished === 0) { - command.success({"data": result}); - } - }; - priv.send(command, "allDocs", null, option, function (err, response) { - var i, row, selector, selected; - if (err) { - return command.error(err); - } - response = response.data; - selector = /\.revision_tree\.json$/; - rows = { - "revision_trees": { - // id.revision_tree.json: { - // id: blabla - // doc: {...} - // } - }, - "document_revisions": { - // id.rev: { - // id: blabla - // rev: 1-1 - // doc: {...} - // } - } - }; - while (response.rows.length > 0) { - // filling rows - row = response.rows.shift(); - selected = selector.exec(row.id); - if (selected) { - selected = selected.input.substring(0, selected.index); - // this is a revision tree - rows.revision_trees[row.id] = { - "id": selected - }; - if (row.doc) { - rows.revision_trees[row.id].doc = row.doc; - } - } else { - // this is a simple revision - rows.document_revisions[row.id] = { - "id": row.id.split(".").slice(0, -1), - "rev": row.id.split(".").slice(-1) - }; - if (row.doc) { - rows.document_revisions[row.id].doc = row.doc; - } - } - } - functions.finished += 1; - for (i in rows.revision_trees) { - if (rows.revision_trees.hasOwnProperty(i)) { - functions.finished += 1; - if (rows.revision_trees[i].doc) { - functions.falseResponseGenerator( - {"data": rows.revision_trees[i].doc}, - functions.fillResultGenerator(rows.revision_trees[i].id) - ); + if (include_revs) { + docum.id = docum.value.timestamp; + //docum.doc = docum.value.doc; } else { - priv.getRevisionTree( - command, - {"_id": rows.revision_trees[i].id}, - option, - functions.fillResultGenerator(rows.revision_trees[i].id) - ); + docum.id = docum.value.doc_id; + //docum.doc = docum.value.doc; } - } - } - functions.success(); + delete docum.value.doc_id; + delete docum.value.timestamp; + delete docum.value.op; + docum.value = docum.value.doc; + //docum.value = {}; + return docum; + }); + return docs_to_query; }); - }; - - // XXX - that.check = function (command) { - command.success(); - }; - - // XXX - that.repair = function (command) { - command.success(); - }; + }; - }); // end RevisionStorage + jIO.addStorage('revision', RevisionStorage); -})); +}(jIO, RSVP, SimpleQuery, ComplexQuery)); \ No newline at end of file diff --git a/test/jio.storage/revisionstorage.tests.js b/test/jio.storage/revisionstorage.tests.js index 9642df03ab9db080f8a053c0c448ef909fb87001..f9ab01fd1b461a85d06a7bb9d344d7304475dcd4 100644 --- a/test/jio.storage/revisionstorage.tests.js +++ b/test/jio.storage/revisionstorage.tests.js @@ -1,1899 +1,3267 @@ -/*jslint indent: 2, maxlen: 80, nomen: true */ -/*global define, jIO, test_util, hex_sha256, RSVP, test, ok, deepEqual, start, - stop, module */ - -// define([module_name], [dependencies], module); -(function (dependencies, module) { - "use strict"; - if (typeof define === 'function' && define.amd) { - return define(dependencies, module); - } - module(jIO, test_util, {hex_sha256: hex_sha256}, RSVP); -}([ - 'jio', - 'test_util', - 'sha256', - 'rsvp', - 'localstorage', - 'revisionstorage' -], function (jIO, util, sha256, RSVP) { +/*jslint nomen: true*/ +/*global Blob*/ +(function (jIO, RSVP, Blob, QUnit) { "use strict"; - - ////////////////////////////////////////////////////////////////////////////// - // Tools - - var tool = { - "deepClone": jIO.util.deepClone, - "uniqueJSONStringify": jIO.util.uniqueJSONStringify, - "readBlobAsBinaryString": jIO.util.readBlobAsBinaryString - }; - - function generateRevisionHash(doc, revisions, deleted_flag) { - var string; - doc = tool.deepClone(doc); - delete doc._rev; - delete doc._revs; - delete doc._revs_info; - string = tool.uniqueJSONStringify(doc) + - tool.uniqueJSONStringify(revisions) + - JSON.stringify(deleted_flag ? true : false); - return sha256.hex_sha256(string); - } - - function isRevision(revision) { - return (/^[0-9]+-[0-9a-zA-Z]+$/).test(revision); - } - - function success(promise) { - return new RSVP.Promise(function (resolve, reject, notify) { - /*jslint unparam: true*/ - promise.then(resolve, resolve, notify); - }, function () { - promise.cancel(); - }); - } - - function unexpectedError(error) { - if (error instanceof Error) { - deepEqual([ - error.name + ": " + error.message, - error - ], "UNEXPECTED ERROR", "Unexpected error"); - } else { - deepEqual(error, "UNEXPECTED ERROR", "Unexpected error"); - } - } - - ////////////////////////////////////////////////////////////////////////////// - // Tests - - module("Revision Storage + Local Storage"); - - test("Post", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision post", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace - }); - - stop(); - - // post without id - shared.revisions = {"start": 0, "ids": []}; - jio.post({}).then(function (response) { - - shared.uuid = response.id; - response.id = ""; - shared.rev = response.rev; - response.rev = ""; - ok(util.isUuid(shared.uuid), "Uuid should look like " + - "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx : " + shared.uuid); - ok(isRevision(shared.rev), "Revision should look like " + - "x-xxxxxxxxxxxxxxxxxxxxxxxxx... : " + shared.rev); - deepEqual( - shared.rev, - "1-" + generateRevisionHash({"_id": shared.uuid}, shared.revisions), - "Check revision value" - ); - deepEqual(response, { - "id": "", - "rev": "", - "method": "post", - "result": "success", - "status": 201, - "statusText": "Created" - }, "Post without id"); - - return jio_local.get({"_id": shared.uuid + "." + shared.rev}); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": shared.uuid + "." + shared.rev - }, "Check document"); - - return jio_local.get({"_id": shared.uuid + ".revision_tree.json"}); - - }).then(function (answer) { - - shared.doc_tree = { - "_id": shared.uuid + ".revision_tree.json", - "children": JSON.stringify([{ - "rev": shared.rev, - "status": "available", - "children": [] - }]) - }; - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // post non empty document - shared.doc = {"_id": "post1", "title": "myPost1"}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" - }, "Post"); - - // check document - shared.doc._id = "post1." + shared.rev; - return jio_local.get(shared.doc); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": shared.doc._id, - "title": "myPost1" - }, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0] = { - "rev": shared.rev, - "status": "available", - "children": [] - }; - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get(shared.doc_tree); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // post and document already exists - shared.doc = {"_id": "post1", "title": "myPost2"}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" - }, "Post and document already exists"); - - // check document - shared.doc._id = "post1." + shared.rev; - return jio_local.get(shared.doc); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get(shared.doc_tree); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // post + revision - shared.doc = {"_id": "post1", "_rev": shared.rev, "title": "myPost2"}; - shared.revisions = {"start": 1, "ids": [shared.rev.split('-')[1]]}; - shared.rev = "2-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" // XXX should be 204 no content - }, "Post + revision"); - - // // keep_revision_history - // ok (false, "keep_revision_history Option Not Implemented"); - - // check document - shared.doc._id = "post1." + shared.rev; - delete shared.doc._rev; - return jio_local.get(shared.doc); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] + var test = QUnit.test, + stop = QUnit.stop, + start = QUnit.start, + ok = QUnit.ok, + expect = QUnit.expect, + deepEqual = QUnit.deepEqual, + equal = QUnit.equal, + module = QUnit.module; + + function putFullDoc(storage, id, doc, attachment_name, attachment) { + return storage.put(id, doc) + .push(function () { + return storage.putAttachment( + id, + attachment_name, + attachment + ); }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get(shared.doc_tree); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // add attachment - return jio_local.putAttachment({ - "_id": "post1." + shared.rev, - "_attachment": "attachment_test", - "_data": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "_content_type": "oh/yeah" - }); - - }).then(function () { - - // post + attachment copy - shared.doc = {"_id": "post1", "_rev": shared.rev, "title": "myPost2"}; - shared.revisions = { - "start": 2, - "ids": [shared.rev.split('-')[1], shared.revisions.ids[0]] - }; - shared.rev = "3-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "result": "success", - "rev": shared.rev, - "status": 201, - "statusText": "Created" - }, "Post + attachment copy"); - - // check attachment - return jio_local.getAttachment({ - "_id": "post1." + shared.rev, - "_attachment": "attachment_test" - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "Check Attachment"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual( - answer.data, - shared.doc_tree, - "Check document tree" - ); - - // post + wrong revision - shared.doc = {"_id": "post1", "_rev": "3-wr3", "title": "myPost3"}; - shared.revisions = {"start": 3, "ids": ["wr3"]}; - shared.rev = "4-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.post(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "post1", - "method": "post", - "rev": shared.rev, - "result": "success", - "status": 201, - "statusText": "Created" - }, "Post + wrong revision"); - - return success(jio_local.get({"_id": "post1.3-wr3"})); - - }).then(function (answer) { - - // check document - deepEqual(answer, { - "error": "not_found", - "id": "post1.3-wr3", - "message": "Cannot find document", - "method": "get", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Check document"); - - // check document - shared.doc._id = "post1." + shared.rev; - delete shared.doc._rev; - - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree._id = "post1.revision_tree.json"; - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": "3-wr3", - "status": "missing", - "children": [{ - "rev": shared.rev, - "status": "available", - "children": [] - }] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get({"_id": "post1.revision_tree.json"}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - }).fail(unexpectedError).always(start); - - }); - - test("Put", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision put", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace - }); + } - stop(); - - // put non empty document - shared.doc = {"_id": "put1", "title": "myPut1"}; - shared.revisions = {"start": 0, "ids": []}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - jio.put(shared.doc).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should 201 Created - }, "Create a document"); - - // check document - shared.doc._id = "put1." + shared.rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree = { - "_id": "put1.revision_tree.json", - "children": [{ - "rev": shared.rev, - "status": "available", - "children": [] - }] - }; - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // put without rev and document already exists - shared.doc = {"_id": "put1", "title": "myPut2"}; - shared.rev = "1-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should be 201 Created - }, "Put same document without revision"); - - - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - - // put + revision - shared.doc = {"_id": "put1", "_rev": shared.rev, "title": "myPut2"}; - shared.revisions = {"start": 1, "ids": [shared.rev.split('-')[1]]}; - shared.rev = "2-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Put + revision"); - - // check document - shared.doc._id = "put1." + shared.rev; - delete shared.doc._rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] - }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // put + wrong revision - shared.doc = {"_id": "put1", "_rev": "3-wr3", "title": "myPut3"}; - shared.revisions = {"start": 3, "ids": ["wr3"]}; - shared.rev = "4-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Put + wrong revision"); - - // check document - shared.doc._id = "put1." + shared.rev; - delete shared.doc._rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": "3-wr3", - "status": "missing", - "children": [{ - "rev": shared.rev, - "status": "available", - "children": [] - }] + module("revisionStorage.post", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // put + revision history - shared.doc = { - "_id": "put1", - //"_revs": ["3-rh3", "2-rh2", "1-rh1"], // same as below - "_revs": {"start": 3, "ids": ["rh3", "rh2", "rh1"]}, - "title": "myPut3" - }; - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": "3-rh3", - "status": 204, - "statusText": "No Content" - }, "Put + revision history"); - - // check document - shared.doc._id = "put1.3-rh3"; - delete shared.doc._revs; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - deepEqual(answer.data, shared.doc, "Check document"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children.unshift({ - "rev": "1-rh1", - "status": "missing", - "children": [{ - "rev": "2-rh2", - "status": "missing", - "children": [{ - "rev": "3-rh3", - "status": "available", - "children": [] - }] - }] + this.revision = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - // add attachment - shared.doc._attachments = { - "att1": { - "length": 1, - "content_type": "text/plain", - "digest": "sha256-ca978112ca1bbdcafac231b39a23dc4da" + - "786eff8147c4e72b9807785afee48bb" - }, - "att2": { - "length": 2, - "content_type": "dont/care", - "digest": "sha256-1e0bbd6c686ba050b8eb03ffeedc64fdc" + - "9d80947fce821abbe5d6dc8d252c5ac" + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } - }; - return RSVP.all([jio_local.putAttachment({ - "_id": "put1.3-rh3", - "_attachment": "att1", - "_data": "a", - "_content_type": "text/plain" - }), jio_local.putAttachment({ - "_id": "put1.3-rh3", - "_attachment": "att2", - "_data": "bc", - "_content_type": "dont/care" - })]); - - }).then(function () { - - // put + revision with attachment - shared.attachments = shared.doc._attachments; - shared.doc = {"_id": "put1", "_rev": "3-rh3", "title": "myPut4"}; - shared.revisions = {"start": 3, "ids": ["rh3", "rh2", "rh1"]}; - shared.rev = "4-" + generateRevisionHash(shared.doc, shared.revisions); - return jio.put(shared.doc); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "put1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Put + revision (document contains attachments)"); - - // check document - shared.doc._id = "put1." + shared.rev; - shared.doc._attachments = shared.attachments; - delete shared.doc._rev; - return jio_local.get({"_id": shared.doc._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc, "Check document"); - - // check attachments - return RSVP.all([jio_local.getAttachment({ - "_id": "put1." + shared.rev, - "_attachment": "att1" - }), jio_local.getAttachment({ - "_id": "put1." + shared.rev, - "_attachment": "att2" - })]); - - }).then(function (answers) { - - deepEqual(answers[0].data.type, "text/plain", "Check attachment 1 type"); - deepEqual(answers[1].data.type, "dont/care", "Check attachment 2 type"); - - return RSVP.all([ - tool.readBlobAsBinaryString(answers[0].data), - tool.readBlobAsBinaryString(answers[1].data) - ]); - - }).then(function (answers) { - - deepEqual(answers[0].target.result, "a", "Check attachment 1 content"); - deepEqual(answers[1].target.result, "bc", "Check attachment 2 content"); - - // check document tree - shared.doc_tree.children = JSON.parse(shared.doc_tree.children); - shared.doc_tree.children[0].children[0].children[0].children.unshift({ - "rev": shared.rev, - "status": "available", - "children": [] }); - shared.doc_tree.children = JSON.stringify(shared.doc_tree.children); - return jio_local.get({"_id": shared.doc_tree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_tree, "Check document tree"); - - }).fail(unexpectedError).always(start); - + } }); - test("Put Attachment", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision putAttachment", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace + test("Verifying simple post works", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + + return jio.post({title: "foo0"}) + .push(function (result) { + //id = result; + return jio.put(result, {title: "foo1"}); + }) + .push(function (result) { + return jio.get(result); + }) + .push(function (res) { + deepEqual(res, { + title: "foo1" + }, "revision storage only retrieves latest version"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (res) { + deepEqual(res.data.rows, [ + { + value: { + title: "foo1" + }, + doc: {}, + id: timestamps[1] + }, + { + value: { + title: "foo0" + }, + doc: {}, + id: timestamps[0] + } + ], + "Two revisions logged with correct metadata"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - // putAttachment without document - shared.revisions = {"start": 0, "ids": []}; - shared.rev_hash = generateRevisionHash({ - "_id": "doc1", - "_attachment": "attmt1", - "_data": "", - "_content_type": "" - }, shared.revisions); - shared.rev = "1-" + shared.rev_hash; - jio.putAttachment({ - "_id": "doc1", - "_attachment": "attmt1", - "_data": "" - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "attmt1", - "id": "doc1", - "method": "putAttachment", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should be 201 Created - }, "PutAttachment without document"); - - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - // check document - deepEqual( - answer.data, - { - "_id": "doc1." + shared.rev, - "_attachments": { - "attmt1": { - "content_type": "", - "length": 0, - "digest": "sha256-e3b0c44298fc1c149afbf4c8996fb9242" + - "7ae41e4649b934ca495991b7852b855" + + ///////////////////////////////////////////////////////////////// + // Attachments + ///////////////////////////////////////////////////////////////// + + module("revisionStorage.attachments", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.blob1 = new Blob(['a']); + this.blob2 = new Blob(['b']); + this.blob3 = new Blob(['ccc']); + this.other_blob = new Blob(['1']); + this.jio = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } - }, - "Check document" - ); - - // check attachment - return jio_local.getAttachment({ - "_id": "doc1." + shared.rev, - "_attachment": "attmt1" - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "", "Check attachment"); - - // adding a metadata to the document - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - answer.data._id = "doc1." + shared.rev; - answer.data.title = "My Title"; - return jio_local.put(answer.data); - - }).then(function () { - - // update attachment - shared.prev_rev = shared.rev; - shared.revisions = {"start": 1, "ids": [shared.rev_hash]}; - shared.rev_hash = generateRevisionHash({ - "_id": "doc1", - "_data": "abc", - "_content_type": "", - "_attachment": "attmt1" - }, shared.revisions); - shared.rev = "2-" + shared.rev_hash; - return jio.putAttachment({ - "_id": "doc1", - "_data": "abc", - "_attachment": "attmt1", - "_rev": shared.prev_rev + } }); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "attmt1", - "id": "doc1", - "method": "putAttachment", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, "Update attachment"); - - // check document - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - deepEqual( - answer.data, - { - "_id": "doc1." + shared.rev, - "title": "My Title", - "_attachments": { - "attmt1": { - "content_type": "", - "length": 3, - "digest": "sha256-ba7816bf8f01cfea414140de5dae2223b00361a3" + - "96177a9cb410ff61f20015ad" + this.revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } } } - }, - "Check document" - ); - - // check attachment - return jio_local.getAttachment({ - "_id": "doc1." + shared.rev, - "_attachment": "attmt1" - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "abc", "Check attachment"); - - // putAttachment new attachment - shared.prev_rev = shared.rev; - shared.revisions = { - "start": 2, - "ids": [shared.rev_hash, shared.revisions.ids[0]] - }; - shared.rev_hash = generateRevisionHash({ - "_id": "doc1", - "_data": "def", - "_attachment": "attmt2", - "_content_type": "" - }, shared.revisions); - shared.rev = "3-" + shared.rev_hash; - return jio.putAttachment({ - "_id": "doc1", - "_data": "def", - "_attachment": "attmt2", - "_rev": shared.prev_rev + } }); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "attmt2", - "id": "doc1", - "method": "putAttachment", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" // XXX should be 201 Created - }, "PutAttachment without document"); - - return jio_local.get({"_id": "doc1." + shared.rev}); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": "doc1." + shared.rev, - "title": "My Title", - "_attachments": { - "attmt1": { - "content_type": "", - "length": 3, - "digest": "sha256-ba7816bf8f01cfea414140de5dae2223b00361a3" + - "96177a9cb410ff61f20015ad" - }, - "attmt2": { - "content_type": "", - "length": 3, - "digest": "sha256-cb8379ac2098aa165029e3938a51da0bcecfc008" + - "fd6795f401178647f96c5b34" + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname } } - }, "Check document"); - - // check attachment - return jio_local.getAttachment({ - "_id": "doc1." + shared.rev, - "_attachment": "attmt2" }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "def", "Check attachment"); - - }).fail(unexpectedError).always(start); - + } }); - test("Get & GetAttachment", function () { - - var shared = {}, jio, jio_local; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision get", - "mode": "memory" - }; + test("Testing proper adding/removing attachments", + function () { + stop(); + expect(10); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + blob2 = this.blob2, + blob1 = this.blob1, + other_blob = this.other_blob, + otherother_blob = new Blob(['abcabc']); + + jio.put("doc", {title: "foo0"}) // 0 + .push(function () { + return jio.put("doc2", {key: "val"}); // 1 + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); // 2 + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob2); // 3 + }) + .push(function () { + return jio.putAttachment("doc", "other_attacheddata", other_blob);// 4 + }) + .push(function () { + return jio.putAttachment( // 5 + "doc", + "otherother_attacheddata", + otherother_blob + ); + }) + .push(function () { + return jio.removeAttachment("doc", "otherother_attacheddata"); // 6 + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo0" + }, "Get does not return any attachment/revision information"); + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function (result) { + deepEqual(result, + blob2, + "Return the attachment information with getAttachment" + ); + return revision.getAttachment( + timestamps[3], + "attacheddata" + ); + }) + .push(function (result) { + deepEqual(result, + blob2, + "Return the attachment information with getAttachment for " + + "current revision" + ); + return revision.getAttachment( + timestamps[2], + "attacheddata" + ); + }, function (error) { + //console.log(error); + ok(false, error); + }) + .push(function (result) { + deepEqual(result, + blob1, + "Return the attachment information with getAttachment for " + + "previous revision" + ); + return jio.getAttachment(timestamps[0], "attached"); + }, function (error) { + ok(false, error); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to go back to a nonexistent timestamp"); + deepEqual(error.message, + "revisionStorage: cannot find object '" + timestamps[0] + "'", + "Error caught by revision storage correctly"); + return jio.getAttachment("doc", "other_attacheddata"); + }) + .push(function (result) { + deepEqual(result, + other_blob, + "Other document successfully queried" + ); + }) + .push(function () { + return jio.getAttachment("doc", "otherother_attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to get a removed attachment"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); + test("get attachment immediately after removing it", + function () { + stop(); + expect(3); + var jio = this.jio, + blob1 = this.blob1; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); + }) + .push(function () { + return jio.removeAttachment("doc", "attacheddata"); + }) + .push(function () { + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc' (removed)", + "Error is handled by revisionstorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace + test("Ordering of put and remove attachments is correct", + function () { + stop(); + expect(1); + var jio = this.jio, + blob1 = this.blob1, + blob2 = this.blob2; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.putAttachment("doc", "data", blob1); + }) + .push(function () { + return jio.removeAttachment("doc", "data"); + }) + .push(function () { + return jio.putAttachment("doc", "data", blob2); + }) + .push(function () { + return jio.getAttachment("doc", "data"); + }) + .push(function (result) { + deepEqual(result, + blob2, + "removeAttachment happens before putAttachment" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - success(jio.get({"_id": "get1"})).then(function (answer) { - - deepEqual(answer, { - "error": "not_found", - "id": "get1", - "message": "Document not found", - "method": "get", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get inexistent document (winner) -> 404 Not Found"); - - return success(jio.getAttachment({"_id": "get1", "_attachment": "get2"})); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "get2", - "error": "not_found", - "id": "get1", - "message": "Document not found", - "method": "getAttachment", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get inexistent attachment (winner) -> 404 Not Found"); - - // adding a document - shared.doctree = { - "_id": "get1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-rev1", - "status": "available", - "children": [] - }]) - }; - shared.doc_myget1 = {"_id": "get1.1-rev1", "title": "myGet1"}; - - - return jio_local.put(shared.doctree); - }).then(function () { - return jio_local.put(shared.doc_myget1); - }).then(function () { - - // get document - shared.doc_myget1_cloned = tool.deepClone(shared.doc_myget1); - shared.doc_myget1_cloned._id = "get1"; - shared.doc_myget1_cloned._rev = "1-rev1"; - shared.doc_myget1_cloned._revisions = {"start": 1, "ids": ["rev1"]}; - shared.doc_myget1_cloned._revs_info = [{ - "rev": "1-rev1", - "status": "available" - }]; - - return jio.get({"_id": "get1"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); + test("Correctness of allAttachments method on current attachments", + function () { + stop(); + expect(14); + var jio = this.jio, + not_revision = this.not_revision, + blob1 = this.blob1, + blob2 = this.blob2, + blob3 = this.blob3, + other_blob = this.other_blob; + + jio.put("doc", {title: "foo0"}) + .push(function () { + return jio.put("doc2", {key: "val"}); + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob1); + }) + .push(function () { + return jio.putAttachment("doc", "attacheddata", blob2); + }) + .push(function () { + return jio.putAttachment("doc", "other_attacheddata", other_blob); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "attacheddata": blob2, + "other_attacheddata": other_blob + }, "allAttachments works as expected."); + return jio.removeAttachment("doc", "attacheddata"); // + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo0" + }, "Get does not return any attachment information"); + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Removed attachments cannot be queried (4)"); + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "other_attacheddata": blob2 + }, "allAttachments works as expected with a removed attachment"); + return jio.putAttachment("doc", "attacheddata", blob3); // + }) + .push(function () { + return not_revision.allDocs(); + }) + .push(function (results) { + var promises = results.data.rows.map(function (data) { + return not_revision.get(data.id); + }); + return RSVP.all(promises); + }) + .push(function (results) { + deepEqual(results, [ + {timestamp: results[0].timestamp, + doc_id: "doc", doc: results[0].doc, op: "put"}, + {timestamp: results[1].timestamp, + doc_id: "doc2", doc: results[1].doc, op: "put"}, + {timestamp: results[2].timestamp, + doc_id: "doc", name: "attacheddata", op: "putAttachment"}, + {timestamp: results[3].timestamp, + doc_id: "doc", name: "attacheddata", op: "putAttachment"}, + {timestamp: results[4].timestamp, + doc_id: "doc", name: "other_attacheddata", op: "putAttachment"}, + {timestamp: results[5].timestamp, + doc_id: "doc", name: "attacheddata", op: "removeAttachment"}, + {timestamp: results[6].timestamp, + doc_id: "doc", name: "attacheddata", op: "putAttachment"} + ], "Other storage can access all document revisions." + ); + }) + .push(function () { + return jio.allDocs(); + }) + .push(function (results) { + equal(results.data.total_rows, + 2, + "Two documents in accessible storage"); + return jio.get(results.data.rows[1].id); + }) + .push(function (result) { + deepEqual(result, { + "key": "val" + }, "Get second document accessible from jio storage"); + + return not_revision.allDocs(); + }) + .push(function (results) { + return RSVP.all(results.data.rows.map(function (d) { + return not_revision.get(d.id); + })); + }) + .push(function (results) { + equal(results.length, 7, "Seven document revisions in storage"); + return jio.remove("doc"); + }) + .push(function () { + return jio.getAttachment("doc", "attacheddata"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Cannot get the attachment of a removed document"); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc' (removed)", + "Error is handled by revisionstorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { - - deepEqual(answer.data, shared.doc_myget1_cloned, "Get document (winner)"); - - // adding two documents - shared.doctree = { - "_id": "get1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-rev1", - "status": "available", - "children": [] - }, { - "rev": "1-rev2", - "status": "available", - "children": [{ - "rev": "2-rev3", - "status": "available", - "children": [] - }] - }]) - }; - shared.doc_myget2 = {"_id": "get1.1-rev2", "title": "myGet2"}; - shared.doc_myget3 = {"_id": "get1.2-rev3", "title": "myGet3"}; - - return jio_local.put(shared.doctree); - }).then(function () { - return jio_local.put(shared.doc_myget2); - }).then(function () { - return jio_local.put(shared.doc_myget3); - }).then(function () { - - // get document - shared.doc_myget3_cloned = tool.deepClone(shared.doc_myget3); - shared.doc_myget3_cloned._id = "get1"; - shared.doc_myget3_cloned._rev = "2-rev3"; - shared.doc_myget3_cloned._revisions = - {"start": 2, "ids": ["rev3", "rev2"]}; - shared.doc_myget3_cloned._revs_info = [{ - "rev": "2-rev3", - "status": "available" - }, { - "rev": "1-rev2", - "status": "available" - }]; - shared.doc_myget3_cloned._conflicts = ["1-rev1"]; - - return jio.get({"_id": "get1"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); - }).then(function (answer) { - - deepEqual(answer.data, - shared.doc_myget3_cloned, - "Get document (winner, after posting another one)"); - - return success(jio.get({"_id": "get1", "_rev": "1-rev0"}, { - "revs_info": true, - "revs": true, - "conflicts": true - })); - - }).then(function (answer) { - - deepEqual(answer, { - "error": "not_found", - "id": "get1", - "message": "Unable to find the document", - "method": "get", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get document (inexistent specific revision)"); - - // get specific document - shared.doc_myget2_cloned = tool.deepClone(shared.doc_myget2); - shared.doc_myget2_cloned._id = "get1"; - shared.doc_myget2_cloned._rev = "1-rev2"; - shared.doc_myget2_cloned._revisions = {"start": 1, "ids": ["rev2"]}; - shared.doc_myget2_cloned._revs_info = [{ - "rev": "1-rev2", - "status": "available" - }]; - shared.doc_myget2_cloned._conflicts = ["1-rev1"]; - return jio.get({"_id": "get1", "_rev": "1-rev2"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); + test("Correctness of allAttachments method on older revisions", + function () { + stop(); + expect(11); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + blob1 = new Blob(['a']), + blob11 = new Blob(['ab']), + blob2 = new Blob(['abc']), + blob22 = new Blob(['abcd']), + timestamps; + + jio.put("doc", {title: "foo0"}) // 0 + .push(function () { + return jio.putAttachment("doc", "data", blob1); + }) + .push(function () { + return jio.putAttachment("doc", "data2", blob2); + }) + .push(function () { + return jio.put("doc", {title: "foo1"}); // 1 + }) + .push(function () { + return jio.removeAttachment("doc", "data2"); + }) + .push(function () { + return jio.put("doc", {title: "foo2"}); // 2 + }) + .push(function () { + return jio.putAttachment("doc", "data", blob11); + }) + .push(function () { + return jio.remove("doc"); // 3 + }) + .push(function () { + return jio.put("doc", {title: "foo3"}); // 4 + }) + .push(function () { + return jio.putAttachment("doc", "data2", blob22); + }) + .push(function () { + return not_revision.allDocs({ + query: "op: put OR op: remove", + sort_on: [["timestamp", "ascending"]], + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return jio.allAttachments("doc"); + }) + .push(function (results) { + deepEqual(results, { + "data": blob11, + "data2": blob22 + }, + "Current state of document is correct"); + + return revision.allAttachments(timestamps[0]); + }) + .push(function (results) { + deepEqual(results, {}, "First version of document has 0 attachments"); + + return revision.allAttachments(timestamps[1]); + }) + .push(function (results) { + deepEqual(results, { + data: blob1, + data2: blob2 + }, "Both attachments are included in allAttachments"); + + return revision.allAttachments(timestamps[2]); + }) + .push(function (results) { + deepEqual(results, { + data: blob1 + }, "Removed attachment does not show up in allAttachments"); + return revision.allAttachments(timestamps[3]); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object '" + timestamps[3] + + "' (removed)", + "Error is handled by revisionstorage."); + }) + .push(function () { + return revision.allAttachments(timestamps[4]); + }) + .push(function (results) { + deepEqual(results, { + data: blob11 + }); + }) + .push(function () { + return revision.allAttachments("not-a-timestamp-or-doc_id"); + }) + .push(function () { + ok(false, "This query should have thrown a 404 error"); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "throws a jio error"); + deepEqual(error.status_code, + 404, + "allAttachments of a removed document throws a 404 error"); + deepEqual(error.message, + "revisionStorage: cannot find object 'not-a-timestamp-or-doc_id'", + "Error is handled by revisionstorage."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { - deepEqual(answer.data, - shared.doc_myget2_cloned, - "Get document (specific revision)"); - // adding an attachment - shared.attmt_myget3 = { - "get2": { - "length": 3, - "digest": "sha256-ba7816bf8f01cfea414140de5dae2223b00361a3" + - "96177a9cb410ff61f20015ad", - "content_type": "oh/yeah" + ///////////////////////////////////////////////////////////////// + // Querying older revisions + ///////////////////////////////////////////////////////////////// + + module("revisionStorage.get", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } } - }; - shared.doc_myget3._attachments = shared.attmt_myget3; - - return jio_local.putAttachment({ - "_id": shared.doc_myget3._id, - "_attachment": "get2", - "_data": "abc", - "_content_type": "oh/yeah" }); - - }).then(function () { - - return jio.getAttachment({"_id": "get1", "_attachment": "get2"}); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "abc", "Get attachment (winner)"); - - // get inexistent attachment specific rev - return success(jio.getAttachment({ - "_id": "get1", - "_attachment": "get2", - "_rev": "1-rev1" - }, { - "revs_info": true, - "revs": true, - "conflicts": true - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "get2", - "error": "not_found", - "id": "get1", - "message": "Unable to get an inexistent attachment", - "method": "getAttachment", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Get inexistent attachment (specific revision) -> 404 Not Found"); - - return jio.getAttachment({ - "_id": "get1", - "_attachment": "get2", - "_rev": "2-rev3" - }, { - "revs_info": true, - "revs": true, - "conflicts": true - }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, - "abc", - "Get attachment (specific revision)"); - - // get document with attachment (specific revision) - delete shared.doc_myget2_cloned._attachments; - return jio.get({"_id": "get1", "_rev": "1-rev2"}, { - "revs_info": true, - "revs": true, - "conflicts": true + this.revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - - }).then(function (answer) { - - deepEqual(answer.data, - shared.doc_myget2_cloned, - "Get document which have an attachment (specific revision)"); - - // get document with attachment (winner) - shared.doc_myget3_cloned._attachments = shared.attmt_myget3; - return jio.get({"_id": "get1"}, { - "revs_info": true, - "revs": true, - "conflicts": true + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } }); + } + }); - }).then(function (answer) { - - deepEqual(answer.data, - shared.doc_myget3_cloned, - "Get document which have an attachment (winner)"); + test("Removing documents before putting them", + function () { + stop(); + expect(4); + var jio = this.jio; + + jio.remove("doc") + .push(function () { + return jio.put("doc2", {title: "foo"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc' (removed)", + "Error is handled by revision storage before reaching console"); + }) + .push(function () { + return jio.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: "doc2", + value: {title: "foo"}, + //timestamp: timestamps[1], + doc: {} + }], "Document that was removed before being put is not retrieved"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).fail(unexpectedError).always(start); + test("Removing documents and then putting them", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + timestamps, + not_revision = this.not_revision; + + jio.remove("doc") + .push(function () { + return jio.put("doc", {title: "foo"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "A put was the most recent edit on 'doc'"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + //id: "doc", + value: {title: "foo"}, + id: timestamps[1], + doc: {} + }, + { + value: {}, + id: timestamps[0], + doc: {} + }], "DOcument that was removed before being put is not retrieved"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }); + test("Handling bad input", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamp; + + jio.put("doc", {title: "foo"}) + .push(function () { + return not_revision.allDocs(); + }) + .push(function (res) { + timestamp = res.data.rows[0].id; + return jio.put(timestamp, {key: "val"}); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "Saving document with timestamp id does not cause issues (1)"); + return revision.get(timestamp); + }) + .push(function (result) { + deepEqual(result, { + title: "foo" + }, "Saving document with timestamp id does not cause issues (2)"); + return revision.get(timestamp); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - test("Remove & Remove Attachment", function () { + test("Getting a non-existent document", + function () { + stop(); + expect(3); + var jio = this.jio; + jio.put("not_doc", {}) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc'", + "Error is handled by revision storage before reaching console"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - var shared = {}, jio, jio_local; + test("Getting a document with timestamp when include_revisions is false", + function () { + stop(); + expect(6); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + jio.put("not_doc", {}) + .push(function () { + return jio.get("doc"); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object 'doc'", + "Error is handled by revision storage before reaching console"); + }) + .push(function () { + return not_revision.allDocs(); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.get(timestamp); + }) + .push(function () { + ok(false, "This statement should not be reached"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Correct status code for getting a non-existent document" + ); + deepEqual(error.message, + "revisionStorage: cannot find object '" + timestamp + "'", + "Error is handled by revision storage before reaching console"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision remove", - "mode": "memory" - }; + test("Creating a document with put and retrieving it with get", + function () { + stop(); + expect(5); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + jio.put("doc", {title: "version0"}) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + equal(timestamps.length, + 1, + "One revision is saved in storage" + ); + return revision.get(timestamps[0]); + }) + .push(function (result) { + deepEqual(result, { + title: "version0" + }, "Get document from revision storage"); + return not_revision.get( + timestamps[0] + ); + }) + .push(function (result) { + deepEqual(result, { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0" + } + }, "Get document from non-revision storage"); + }) + .push(function () { + return jio.get("non-existent-doc"); + }) + .push(function () { + ok(false, "This should have thrown an error"); + }, function (error) { + //console.log(error); + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Can't access non-existent document" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); + test("Updating a document with include revisions", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + t_id; + jio.put("doc", {title: "version0"}) + .push(function () { + return revision.put("doc", {title: "version1"}); + }) + .push(function () { + return not_revision.allDocs({sort_on: [["timestamp", "ascending"]]}); + }) + .push(function (results) { + t_id = results.data.rows[0].id; + return revision.put(t_id, {title: "version0.1"}); + }) + .push(function () { + return jio.put(t_id, {title: "label0"}); + }) + .push(function () { + return revision.put("1234567891012-abcd", {k: "v"}); + }) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]], + select_list: ["timestamp", "op", "doc_id", "doc"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[0], + doc: {}, + value: { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0" + } + } + }, + { + id: timestamps[1], + doc: {}, + value: { + timestamp: timestamps[1], + op: "put", + doc_id: "doc", + doc: { + title: "version1" + } + } + }, + { + id: timestamps[2], + doc: {}, + value: { + timestamp: timestamps[2], + op: "put", + doc_id: "doc", + doc: { + title: "version0.1" + } + } + }, + { + id: timestamps[3], + doc: {}, + value: { + timestamp: timestamps[3], + op: "put", + doc_id: timestamps[0], + doc: { + title: "label0" + } + } + }, + { + id: timestamps[4], + doc: {}, + value: { + timestamp: timestamps[4], + op: "put", + doc_id: "1234567891012-abcd", + doc: { + k: "v" + } + } + } + ], "Documents stored with correct metadata"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio_local = jIO.createJIO(shared.local_storage_description, { - "workspace": shared.workspace + test("Retrieving older revisions with get", + function () { + stop(); + expect(7); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + + return jio.put("doc", {title: "t0", subtitle: "s0"}) + .push(function () { + return jio.put("doc", {title: "t1", subtitle: "s1"}); + }) + .push(function () { + return jio.put("doc", {title: "t2", subtitle: "s2"}); + }) + .push(function () { + jio.remove("doc"); + }) + .push(function () { + return jio.put("doc", {title: "t3", subtitle: "s3"}); + }) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (result) { + deepEqual(result, { + title: "t3", + subtitle: "s3" + }, "Get returns latest revision"); + return revision.get(timestamps[0]); + }, function (err) { + ok(false, err); + }) + .push(function (result) { + deepEqual(result, { + title: "t0", + subtitle: "s0" + }, "Get returns first version"); + return revision.get(timestamps[1]); + }) + .push(function (result) { + deepEqual(result, { + title: "t1", + subtitle: "s1" + }, "Get returns second version"); + return revision.get(timestamps[2]); + }, function (err) { + ok(false, err); + }) + .push(function (result) { + deepEqual(result, { + title: "t2", + subtitle: "s2" + }, "Get returns third version"); + return revision.get(timestamps[3]); + }, function (err) { + ok(false, err); + }) + .push(function () { + ok(false, "This should have thrown a 404 error"); + return revision.get(timestamps[4]); + }, + function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + deepEqual(error.status_code, + 404, + "Error if you try to go back more revisions than what exists"); + return revision.get(timestamps[4]); + }) + .push(function (result) { + deepEqual(result, { + title: "t3", + subtitle: "s3" + }, "Get returns latest version"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - // 1. remove document without revision - success(jio.remove({"_id": "remove1"})).then(function (answer) { - - deepEqual(answer, { - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "remove", - "reason": "No document revision was provided", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove document without revision -> 409 Conflict"); - - // 2. remove attachment without revision - return success(jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove2" - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove2", - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "removeAttachment", - "reason": "No document revision was provided", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove attachment without revision -> 409 Conflict"); - - // adding a document with attachments - shared.doc_myremove1 = { - "_id": "remove1.1-veryoldrev", - "title": "myRemove1" - }; - - return jio_local.put(shared.doc_myremove1); - - }).then(function () { - - shared.doc_myremove1._id = "remove1.2-oldrev"; - shared.attachment_remove2 = { - "length": 3, - "digest": "md5-dontcare", - "content_type": "oh/yeah" - }; - shared.attachment_remove3 = { - "length": 5, - "digest": "sha256-383395a769131d15c1c6fc57c6abdb759ace9809" + - "c1ad20d1f491d90f7f02650e", - "content_type": "he/ho" - }; - shared.doc_myremove1._attachments = { - "remove2": shared.attachment_remove2, - "remove3": shared.attachment_remove3 - }; - - return jio_local.put(shared.doc_myremove1); - - }).then(function () { - - return jio_local.putAttachment({ - "_id": "remove1.2-oldrev", - "_attachment": "remove2", - "_data": "abc", - "_content_type": "oh/yeah" - }); + test("verifying updates correctly when puts are done in parallel", + function () { + stop(); + expect(8); + var jio = this.jio, + not_revision = this.not_revision; + + jio.put("bar", {"title": "foo0"}) + .push(function () { + return RSVP.all([ + jio.put("bar", {"title": "foo1"}), + jio.put("bar", {"title": "foo2"}), + jio.put("bar", {"title": "foo3"}), + jio.put("bar", {"title": "foo4"}), + jio.put("barbar", {"title": "attr0"}), + jio.put("barbar", {"title": "attr1"}), + jio.put("barbar", {"title": "attr2"}), + jio.put("barbar", {"title": "attr3"}) + ]); + }) + .push(function () {return jio.get("bar"); }) + .push(function (result) { + ok(result.title !== "foo0", "Title should have changed from foo0"); + }) + .push(function () { + return not_revision.allDocs({ + query: "", + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + equal(results.data.total_rows, + 9, + "All nine versions exist in storage"); + return not_revision.get(results.data.rows[0].id); + }) + .push(function (results) { + deepEqual(results, { + doc_id: "bar", + doc: { + title: "foo0" + }, + timestamp: results.timestamp, + op: "put" + }, "The first item in the log is pushing bar's title to 'foo0'"); + return jio.remove("bar"); + }) + .push(function () { + return jio.get("bar"); + }) + .push(function () { + return jio.get("barbar"); + }, function (error) { + ok(error instanceof jIO.util.jIOError, "Correct type of error"); + equal(error.status_code, 404, "Correct error status code returned"); + return jio.get("barbar"); + }) + .push(function (result) { + ok(result.title !== undefined, "barbar exists and has proper form"); + return not_revision.allDocs({ + query: "", + sort_on: [["op", "descending"]] + }); + }) + .push(function (results) { + equal(results.data.total_rows, + 10, + "Remove operation is recorded"); + return not_revision.get(results.data.rows[0].id); + }) + .push(function (result) { + deepEqual(result, { + doc_id: "bar", + timestamp: result.timestamp, + op: "remove" + }); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function () { + test("Getting after attachments have been put", + function () { + stop(); + expect(4); + var jio = this.jio, + not_revision = this.not_revision, + revision = this.revision, + blob = new Blob(['a']), + edit_log; + + jio.put("doc", {"title": "foo0"}) + .push(function () { + return jio.putAttachment("doc", "attachment", blob); + }) + .push(function () { + return jio.removeAttachment("doc", "attachment", blob); + }) + .push(function () { + return jio.get("doc"); + }) + .push(function (res) { + deepEqual(res, + {title: "foo0"}, + "Correct information returned"); + return not_revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + edit_log = results.data.rows; + return revision.get(edit_log[0].id); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + return revision.get(edit_log[1].id); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + return revision.get(edit_log[2].id); + }) + .push(function (result) { + deepEqual(result, {title: "foo0"}); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - return jio_local.putAttachment({ - "_id": "remove1.2-oldrev", - "_attachment": "remove3", - "_data": "defgh", - "_content_type": "he/ho" - }); - }).then(function () { - - // add document tree - shared.doctree = { - "_id": "remove1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-veryoldrev", - "status": "available", - "children": [{ - "rev": "2-oldrev", - "status": "available", - "children": [] - }] - }]) - }; - - return jio_local.put(shared.doctree); - - }).then(function () { - - // 3. remove inexistent attachment - return success(jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove0", - "_rev": "2-oldrev" - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove0", - "error": "not_found", - "id": "remove1", - "message": "Unable to remove an inexistent attachment", - "method": "removeAttachment", - "reason": "missing", - "result": "error", - "status": 404, - "statusText": "Not Found" - }, "Remove inexistent attachment -> 404 Not Found"); - - // 4. remove existing attachment - shared.rev_hash = generateRevisionHash({ - "_id": "remove1", - "_attachment": "remove2" - }, {"start": 2, "ids": ["oldrev", "veryoldrev"]}); - - return jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove2", - "_rev": "2-oldrev" + ///////////////////////////////////////////////////////////////// + // Querying older revisions + ///////////////////////////////////////////////////////////////// + + module("revisionStorage.allDocs", { + setup: function () { + // create storage of type "revision" with memory as substorage + this.dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: this.dbname + } + } + } + } }); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove2", - "id": "remove1", - "method": "removeAttachment", - "result": "success", - "rev": "3-" + shared.rev_hash, - "status": 204, - "statusText": "No Content" - }, "Remove existing attachment"); - - shared.doctree = { - "_id": "remove1.revision_tree.json", - "children": JSON.stringify([{ - "rev": "1-veryoldrev", - "status": "available", - "children": [{ - "rev": "2-oldrev", - "status": "available", - "children": [{ - "rev": "3-" + shared.rev_hash, - "status": "available", - "children": [] - }] - }] - }]) - }; - - // 5. check if document tree has been updated correctly - return jio_local.get({"_id": shared.doctree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doctree, "Check document tree"); - - // 6. check if the attachment still exists - return jio_local.getAttachment({ - "_id": "remove1.2-oldrev", - "_attachment": "remove2" + this.revision = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: this.dbname + } + } + } + } }); - - }).then(function (answer) { - - return tool.readBlobAsBinaryString(answer.data); - - }).then(function (event) { - - deepEqual(event.target.result, "abc", "Check attachment -> still exists"); - - // 7. check if document is updated - return jio_local.get({"_id": "remove1.3-" + shared.rev_hash}); - - }).then(function (answer) { - - deepEqual(answer.data, { - "_id": "remove1.3-" + shared.rev_hash, - "title": "myRemove1", - "_attachments": { - "remove3": shared.attachment_remove3 + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: this.dbname + } } - }, "Check document"); - - // 8. remove document with wrong revision - return success(jio.remove({"_id": "remove1", "_rev": "1-a"})); - - }).then(function (answer) { - - deepEqual(answer, { - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "remove", - "reason": "Document is missing", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove document with wrong revision -> 409 Conflict"); - - // 9. remove attachment wrong revision - return success(jio.removeAttachment({ - "_id": "remove1", - "_attachment": "remove2", - "_rev": "1-a" - })); - - }).then(function (answer) { - - deepEqual(answer, { - "attachment": "remove2", - "error": "conflict", - "id": "remove1", - "message": "Document update conflict", - "method": "removeAttachment", - "reason": "Document is missing", - "result": "error", - "status": 409, - "statusText": "Conflict" - }, "Remove attachment with wrong revision -> 409 Conflict"); - - // 10. remove document - shared.last_rev = "3-" + shared.rev_hash; - shared.rev_hash = generateRevisionHash( - {"_id": "remove1"}, - {"start": 3, "ids": [shared.rev_hash, "oldrev", "veryoldrev"]}, - true - ); - return jio.remove({"_id": "remove1", "_rev": shared.last_rev}); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "remove1", - "method": "remove", - "result": "success", - "rev": "4-" + shared.rev_hash, - "status": 204, - "statusText": "No Content" - }, "Remove document"); - - // 11. check document tree - shared.doctree.children = JSON.parse(shared.doctree.children); - shared.doctree.children[0].children[0].children[0].children.unshift({ - "rev": "4-" + shared.rev_hash, - "status": "deleted", - "children": [] }); - shared.doctree.children = JSON.stringify(shared.doctree.children); - return jio_local.get({"_id": shared.doctree._id}); - - }).then(function (answer) { - - deepEqual(answer.data, shared.doctree, "Check document tree"); - - }).fail(unexpectedError).always(start); - + } }); + test("Putting a document and retrieving it with allDocs", + function () { + stop(); + expect(7); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + jio.put("doc", {title: "version0"}) + .push(function () { + return not_revision.allDocs({ + query: "doc_id: doc", + select_list: ["timestamp"] + }); + }) + .push(function (results) { + timestamp = results.data.rows[0].value.timestamp; + }) + .push(function () { + return RSVP.all([ + jio.allDocs(), + jio.allDocs({query: "title: version0"}), + jio.allDocs({limit: [0, 1]}), + jio.allDocs({}) + ]); + }) + .push(function (results) { + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; + }) + .push(function (results) { + equal(results.data.total_rows, + 1, + "Exactly one result returned"); + deepEqual(results.data.rows[0], { + doc: {}, + value: {}, + //timestamp: timestamp, + id: "doc" + }, + "Correct document format is returned." + ); + return not_revision.allDocs(); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + equal(results.data.total_rows, + 1, + "Exactly one result returned"); + return not_revision.get(timestamp); + }) + .push(function (result) { + deepEqual(result, { + doc_id: "doc", + doc: { + title: "version0" + }, + timestamp: timestamp, + op: "put" + }, + "When a different type of storage queries revisionstorage, all " + + "metadata is returned correctly" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - test("allDocs", function () { - - var shared = {}, jio; - - shared.workspace = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision alldocs", - "mode": "memory" - }; - - jio = jIO.createJIO({ - "type": "revision", - "sub_storage": shared.local_storage_description - }, {"workspace": shared.workspace}); - - stop(); - - // adding 3 documents - jio.put({"_id": "yes"}).then(function (answer) { - - shared.rev1 = answer.rev; - - return jio.put({"_id": "no"}); - - }).then(function (answer) { - - shared.rev2 = answer.rev; - - return jio.put({"_id": "maybe"}); + test("Putting doc with troublesome properties and retrieving with allDocs", + function () { + stop(); + expect(1); + var jio = this.jio; + jio.put("doc", { + title: "version0", + doc_id: "bar", + _doc_id: "bar2", + timestamp: "foo", + _timestamp: "foo2", + id: "baz", + _id: "baz2", + __id: "baz3", + op: "zop" + }) + .push(function () { + return jio.allDocs({ + query: "title: version0 AND _timestamp: >= 0", + select_list: ["title", "doc_id", "_doc_id", "timestamp", + "_timestamp", "id", "_id", "__id", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + //timestamp: timestamp, + value: { + title: "version0", + doc_id: "bar", + _doc_id: "bar2", + timestamp: "foo", + _timestamp: "foo2", + id: "baz", + _id: "baz2", + __id: "baz3", + op: "zop" + } + }], + "Poorly-named properties are not overwritten in allDocs call"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { + test("Putting a document, revising it, and retrieving revisions with allDocs", + function () { + stop(); + expect(10); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps; + jio.put("doc", { + title: "version0", + subtitle: "subvers0" + }) + .push(function () { + return jio.put("doc", { + title: "version1", + subtitle: "subvers1" + }); + }) + .push(function () { + return jio.put("doc", { + title: "version2", + subtitle: "subvers2" + }); + }) + .push(function () { + return not_revision.allDocs({ + select_list: ["timestamp"], + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.value.timestamp; + }); + }) + .push(function () { + return RSVP.all([ + jio.allDocs({select_list: ["title", "subtitle"]}), + jio.allDocs({ + query: "", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + query: "title: version2", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + query: "NOT (title: version1)", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + query: "(NOT (subtitle: subvers1)) AND (NOT (title: version0))", + select_list: ["title", "subtitle"] + }), + jio.allDocs({ + limit: [0, 1], + sort_on: [["title", "ascending"]], + select_list: ["title", "subtitle"] + }) + ]); + }) + .push(function (results) { + var ind = 0; + for (ind = 0; ind < results.length - 1; ind += 1) { + deepEqual(results[ind], + results[ind + 1], + "Each query returns exactly the same correct output" + ); + } + return results[0]; + }) + .push(function (results) { + equal(results.data.total_rows, + 1, + "Exactly one result returned"); + deepEqual(results.data.rows[0], { + value: { + title: "version2", + subtitle: "subvers2" + }, + doc: {}, + //timestamp: timestamps[2], + id: "doc" + }, + "Correct document format is returned." + ); + }) + .push(function () { + return revision.allDocs({ + query: "", + select_list: ["title", "subtitle"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, + 3, + "Querying with include_revisions retrieves all versions"); + deepEqual(results.data.rows, [ + { + //id: results.data.rows[0].id, + value: { + title: "version2", + subtitle: "subvers2" + }, + id: timestamps[2], + doc: {} + }, + { + //id: results.data.rows[1].id, + value: { + title: "version1", + subtitle: "subvers1" + }, + id: timestamps[1], + doc: {} + }, + { + //id: results.data.rows[2].id, + value: { + title: "version0", + subtitle: "subvers0" + }, + id: timestamps[0], + doc: {} + } + ], "Full version revision is included."); + + return not_revision.allDocs({ + sort_on: [["title", "ascending"]] + }); + }) + .push(function (results) { + return RSVP.all(results.data.rows.map(function (d) { + return not_revision.get(d.id); + })); + }) + .push(function (results) { + deepEqual(results, [ + { + timestamp: timestamps[0], + op: "put", + doc_id: "doc", + doc: { + title: "version0", + subtitle: "subvers0" + } + }, + { + timestamp: timestamps[1], + op: "put", + doc_id: "doc", + doc: { + title: "version1", + subtitle: "subvers1" + } + }, + { + timestamp: timestamps[2], + op: "put", + doc_id: "doc", + doc: { + title: "version2", + subtitle: "subvers2" + } + } + ], + "A different storage type can retrieve all versions as expected."); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - shared.rev3 = answer.rev; - // adding conflicts - return jio.put({"_id": "maybe"}); + test( + "Putting and removing documents, latest revisions and no removed documents", + function () { + stop(); + expect(3); + var jio = this.jio, + not_revision = this.not_revision, + timestamps; + + jio.put("doc_a", { + title_a: "rev0", + subtitle_a: "subrev0" + }) + .push(function () { + return jio.put("doc_a", { + title_a: "rev1", + subtitle_a: "subrev1" + }); + }) + .push(function () { + return jio.put("doc_b", { + title_b: "rev0", + subtitle_b: "subrev0" + }); + }) + .push(function () { + return jio.remove("doc_b"); + }) + .push(function () { + return jio.put("doc_c", { + title_c: "rev0", + subtitle_c: "subrev0" + }); + }) + .push(function () { + return jio.put("doc_c", { + title_c: "rev1", + subtitle_c: "subrev1" + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + equal(results.data.total_rows, + 2, + "Only two non-removed unique documents exist." + ); + deepEqual(results.data.rows, [ + { + id: "doc_c", + value: {}, + //timestamp: timestamps[5], + doc: {} + }, + { + id: "doc_a", + value: {}, + //timestamp: timestamps[1], + doc: {} + } + ], + "Empty query returns latest revisions (and no removed documents)"); + equal(timestamps.length, + 6, + "Correct number of revisions logged"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + } + ); + + ///////////////////////////////////////////////////////////////// + // Complex Queries + ///////////////////////////////////////////////////////////////// + + test("More complex query with different options (without revision queries)", + function () { + stop(); + expect(2); + var jio = this.jio, + docs = [ + { + "date": 1, + "type": "foo", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + }, + { + "date": 2, + "type": "barbar", + "title": "third_doc" + } + ], + blobs = [ + new Blob(['a']), + new Blob(['bcd']), + new Blob(['eeee']) + ]; + jio.put("doc", {}) // 0 + .push(function () { + return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); // 1,2 + }) + .push(function () { + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]);// 3,4 + }) + .push(function () { + return putFullDoc(jio, "third_doc", docs[2], "data", blobs[2]); // 5,6 + }) + .push(function () { + return jio.allDocs({ + query: "NOT (date: > 2)", + select_list: ["date", "non-existent-key"], + sort_on: [["date", "ascending"], + ["non-existent-key", "ascending"] + ] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 3); + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + //timestamp: timestamps[2], + value: {date: 1} + }, + { + doc: {}, + id: "third_doc", + //timestamp: timestamps[6], + value: {date: 2} + }, + { + doc: {}, + id: "second_doc", + //timestamp: timestamps[4], + value: {date: 2} + } + ], + "Query gives correct results in correct order"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function () { + ///////////////////////////////////////////////////////////////// + // Complex Queries with Revision Querying + ///////////////////////////////////////////////////////////////// + + test("More complex query with different options (with revision queries)", + function () { + stop(); + expect(3); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + docs = [ + { + "date": 1, + "type": "foo", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + } + ], + blobs = [ + new Blob(['a']), + new Blob(['bcd']), + new Blob(['a2']), + new Blob(['bcd2']), + new Blob(['a3']) + ]; + jio.put("doc", {})// 0 + .push(function () {// 1,2 + return putFullDoc(jio, "doc", docs[0], "data", blobs[0]); + }) + .push(function () {// 3,4 + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[1]); + }) + .push(function () { + docs[0].date = 4; + docs[0].type = "foo2"; + docs[1].date = 4; + docs[1].type = "bar2"; + }) + .push(function () {// 5,6 + return putFullDoc(jio, "doc", docs[0], "data", blobs[2]); + }) + .push(function () {// 7 + return jio.remove("second_doc"); + }) + .push(function () {// 8,9 + return putFullDoc(jio, "second_doc", docs[1], "data", blobs[3]); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["op", "doc_id", "timestamp"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[9], + value: { + "op": "putAttachment", + "doc_id": "second_doc", + "timestamp": timestamps[9] + } + }, + { + doc: {}, + id: timestamps[8], + value: { + "op": "put", + "doc_id": "second_doc", + "timestamp": timestamps[8] + } + }, + { + doc: {}, + id: timestamps[7], + value: { + "op": "remove", + "doc_id": "second_doc", + "timestamp": timestamps[7] + } + }, + { + doc: {}, + id: timestamps[6], + value: { + "op": "putAttachment", + "doc_id": "doc", + "timestamp": timestamps[6] + } + }, + { + doc: {}, + id: timestamps[5], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps[5] + } + }, + { + doc: {}, + id: timestamps[4], + value: { + "op": "putAttachment", + "doc_id": "second_doc", + "timestamp": timestamps[4] + } + }, + { + doc: {}, + id: timestamps[3], + value: { + "op": "put", + "doc_id": "second_doc", + "timestamp": timestamps[3] + } + }, + { + doc: {}, + id: timestamps[2], + value: { + "op": "putAttachment", + "doc_id": "doc", + "timestamp": timestamps[2] + } + }, + { + doc: {}, + id: timestamps[1], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps[1] + } + }, + { + doc: {}, + id: timestamps[0], + value: { + "op": "put", + "doc_id": "doc", + "timestamp": timestamps[0] + } + } + ], "All operations are logged correctly"); + var promises = results.data.rows + .filter(function (doc) { + return (doc.value.op === "put"); + }) + .map(function (data) { + return not_revision.get(data.id); + }); + return RSVP.all(promises) + .then(function (results) { + return results.map(function (docum) { + return docum.doc; + }); + }); + }) + .push(function (results) { + deepEqual(results, + [ + { + "date": 4, + "type": "bar2", + "title": "second_doc" + }, + { + "date": 4, + "type": "foo2", + "title": "doc" + }, + { + "date": 2, + "type": "bar", + "title": "second_doc" + }, + { + "date": 1, + "type": "foo", + "title": "doc" + }, + {} + ], "All versions of documents are stored correctly"); + }) + .push(function () { + return revision.allDocs({ + query: "NOT (date: >= 2 AND date: <= 3) AND " + + "(date: = 1 OR date: = 4)", + select_list: ["date", "non-existent-key", "type", "title"], + sort_on: [["date", "descending"]] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[9], + value: { + date: 4, + title: "second_doc", + type: "bar2" + } + }, + { + doc: {}, + id: timestamps[8], + value: { + date: 4, + title: "second_doc", + type: "bar2" + } + }, + { + doc: {}, + id: timestamps[6], + value: { + date: 4, + title: "doc", + type: "foo2" + } + }, + { + doc: {}, + id: timestamps[5], + value: { + date: 4, + title: "doc", + type: "foo2" + } + }, + + { + doc: {}, + id: timestamps[2], + value: { + date: 1, + title: "doc", + type: "foo" + } + }, + { + doc: {}, + id: timestamps[1], + value: { + date: 1, + title: "doc", + type: "foo" + } + } + ], + "Query gives correct results in correct order"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - // adding 2 attachments - return jio.putAttachment({ - "_id": "yes", - "_attachment": "blue", - "_mimetype": "text/plain", - "_rev": shared.rev1, - "_data": "sky" - }); + test( + "allDocs with include_revisions with an attachment on a removed document", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + blob = new Blob(['a']), + timestamps; + + jio.put("document", {title: "foo"}) + .push(function () { + return jio.remove("document"); + }) + .push(function () { + return jio.putAttachment("document", "attachment", blob); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[2], + doc: {}, + value: {} + }, + { + id: timestamps[1], + doc: {}, + value: {} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo"} + }], + "Attachment on removed document is handled correctly" + ); + return not_revision.allDocs({select_list: ["doc"]}); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + } + ); + + test("allDocs with include_revisions with a removed attachment", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + blob = new Blob(['a']), + timestamps, + not_revision = this.not_revision; + + jio.put("document", {title: "foo"}) + .push(function () { + return jio.putAttachment("document", "attachment", blob); + }) + .push(function () { + return jio.removeAttachment("document", "attachment"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({select_list: ["title"]}); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[2], + doc: {}, + value: {title: "foo"} + }, + { + id: timestamps[1], + doc: {}, + value: {title: "foo"} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo"} + }], + "Attachment on removed document is handled correctly" + ); + }) + .push(function () { + return jio.allAttachments("document"); + }) + .push(function (results) { + deepEqual(results, {}, "No non-removed attachments"); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { + test("allDocs with include_revisions only one document", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + timestamps, + not_revision = this.not_revision; + + jio.put("doc a", {title: "foo0"}) + .push(function () { + return jio.put("doc a", {title: "foo1"}); + }) + .push(function () { + return jio.put("doc b", {title: "bar0"}); + }) + .push(function () { + return jio.put("doc b", {title: "bar1"}); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({ + query: 'doc_id: "doc a"', + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + id: timestamps[1], + doc: {}, + value: {title: "foo1"} + }, + { + id: timestamps[0], + doc: {}, + value: {title: "foo0"} + }], + "Only specified document revision revision is returned" + ); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - shared.rev1 = answer.rev; + test("Parallel edits will not break anything", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + blob1 = new Blob(['ab']), + blob2 = new Blob(['abc']), + blob3 = new Blob(['abcd']); + + jio.put("doc", {k: "v0"}) + .push(function () { + return RSVP.all([ + jio.put("doc", {k: "v"}), + jio.putAttachment("doc", "data", blob1), + jio.putAttachment("doc", "data2", blob2), + jio.putAttachment("doc", "data", blob3), + jio.removeAttachment("doc", "data"), + jio.removeAttachment("doc", "data2"), + jio.remove("doc"), + jio.remove("doc"), + jio.put("doc", {k: "v"}), + jio.put("doc", {k: "v"}), + jio.put("doc2", {k: "foo"}), + jio.remove("doc"), + jio.remove("doc") + ]); + }) + + .push(function () { + ok(true, "No errors thrown."); + return revision.allDocs(); + }) + .push(function (results) { + var res = results.data.rows; + equal(res.length, + 14, + "All edits are recorded regardless of ordering"); + return jio.allDocs(); + }) + + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - return jio.putAttachment({ - "_id": "no", - "_attachment": "Heeeee!", - "_mimetype": "text/plain", - "_rev": shared.rev2, - "_data": "Hooooo!" - }); + test("Adding second query storage on top of revision", + function () { + stop(); + expect(1); + var jio = this.jio; + return jio.put("doca", {title: "foo0", date: 0}) + .push(function () { + return jio.put("docb", {title: "bar0", date: 0}); + }) + .push(function () { + return jio.put("docb", {title: "bar1", date: 0}); + }) + .push(function () { + return jio.put("doca", {title: "foo1", date: 1}); + }) + .push(function () { + return jio.put("docb", {title: "bar2", date: 2}); + }) + .push(function () { + return jio.allDocs({ + query: "title: foo1 OR title: bar2", + select_list: ["title"], + sort_on: [["date", "ascending"]], + limit: [0, 1] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doca", + value: {title: "foo1"} + } + ]); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { - shared.rev2 = answer.rev; - shared.rows = { - "total_rows": 3, - "rows": [{ - "id": "maybe", - "key": "maybe", - "value": { - "rev": shared.rev3 - } - }, { - "id": "no", - "key": "no", - "value": { - "rev": shared.rev2 - } - }, { - "id": "yes", - "key": "yes", - "value": { - "rev": shared.rev1 + module("revisionStorage.Full-Example", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.blob1 = new Blob(['a']); + this.blob2 = new Blob(['b']); + this.blob3 = new Blob(['ccc']); + this.other_blob = new Blob(['1']); + + this.jio = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } } - }] - }; - - return jio.allDocs(); - - }).then(function (answer) { - - answer.data.rows.sort(function (a, b) { - return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; + } }); - deepEqual(answer.data, shared.rows, "allDocs"); - - shared.rows.rows[0].doc = { - "_id": "maybe", - "_rev": shared.rev3 - }; - shared.rows.rows[1].doc = { - "_id": "no", - "_rev": shared.rev2, - "_attachments": { - "Heeeee!": { - "content_type": "text/plain", - "digest": "sha256-bb333a2679b9537548d359d3f0f8e5cdee541bc8" + - "bb38bd5091e889453c15bd5d", - "length": 7 + this.revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } } } - }; - shared.rows.rows[2].doc = { - "_id": "yes", - "_rev": shared.rev1, - "_attachments": { - "blue": { - "content_type": "text/plain", - "digest": "sha256-05f514fae7ca5710f9e9289a20a5c9b372af781b" + - "fc94dd23d9cb8a044122460f", - "length": 3 + }); + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname } } - }; - - return jio.allDocs({"include_docs": true}); - - }).then(function (answer) { - - answer.data.rows.sort(function (a, b) { - return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; }); - deepEqual(answer.data, shared.rows, "allDocs + include docs"); - - }).fail(unexpectedError).always(start); - + } }); - - test("Scenario", function () { - - var shared = {}, jio, jio2; - - shared.workspace1 = {}; - shared.workspace2 = {}; - shared.local_storage_description = { - "type": "local", - "username": "revision scenario", - "mode": "memory" - }; - shared.revision_storage_desciption = { - "type": "revision", - "sub_storage": shared.local_storage_description - }; - - jio = jIO.createJIO(shared.revision_storage_desciption, { - "workspace": shared.workspace1 + test("Retrieving revision with attachments", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + timestamps, + not_revision = this.not_revision, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ], + blobs2 = [ + new Blob(['abcdef']), + new Blob(['abcdefg']), + new Blob(['abcdefgh']), + new Blob(['abcdefghi']), + new Blob(['abcdefghij']) + ]; + putFullDoc(jio, "doc", {title: "bar"}, "data", blobs1[0]) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar0"}, "data", blobs1[1]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar1"}, "data", blobs1[2]); + }) + .push(function () { + return putFullDoc(jio, "doc2", {title: "foo0"}, "data", blobs2[0]); + }) + .push(function () { + return putFullDoc(jio, "doc2", {title: "foo1"}, "data", blobs2[0]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar2"}, "data", blobs1[3]); + }) + .push(function () { + return putFullDoc(jio, "doc", {title: "bar3"}, "data", blobs1[4]); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return revision.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[13], + value: {title: "bar3"} + }, + { + doc: {}, + id: timestamps[12], + value: {title: "bar3"} + }, + { + doc: {}, + id: timestamps[11], + value: {title: "bar2"} + }, + { + doc: {}, + id: timestamps[10], + value: {title: "bar2"} + }, + { + doc: {}, + id: timestamps[9], + value: {title: "foo1"} + }, + { + doc: {}, + id: timestamps[8], + value: {title: "foo1"} + }, + { + doc: {}, + id: timestamps[7], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[6], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[5], + value: {title: "bar1"} + }, + { + doc: {}, + id: timestamps[4], + value: {title: "bar1"} + }, + { + doc: {}, + id: timestamps[3], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[2], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[1], + value: {title: "bar"} + }, + { + doc: {}, + id: timestamps[0], + value: {title: "bar"} + } + ], + "allDocs with include_revisions should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); }); - stop(); - - // new application - ok(jio, "I open my application with revision and localstorage"); - // put non empty document A-1 - shared.doc = {"_id": "sample1", "title": "mySample1"}; - shared.revisions = {"start": 0, "ids": []}; - shared.hex = generateRevisionHash(shared.doc, shared.revisions); - shared.rev = "1-" + shared.hex; + test("Retrieving revision with attachments with less straightforward ordering", + function () { + stop(); + expect(1); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ]; + jio.put("doc", {title: "bar"}) + .push(function () { + return jio.put("doc", {title: "bar0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[0]); + }) + .push(function () { + return jio.put("doc2", {title: "foo0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[1]); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + + .push(function () { + return revision.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[4], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[3], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[2], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[1], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[0], + value: {title: "bar"} + } + ], + "allDocs with include_revisions should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - jio.put(shared.doc).then(function (answer) { - deepEqual( - answer, - { - "id": "sample1", - "method": "put", - "result": "success", - "rev": shared.rev, - "status": 204, - "statusText": "No Content" - }, - "Then, I create a new document (no attachment), " + - "my application keeps the revision in memory" - ); - - // open new tab (JIO) - jio2 = jIO.createJIO(shared.revision_storage_desciption, { - "workspace": shared.workspace2 - }); - - // Create a new JIO in a new tab - ok(jio2, "Now, I am opening a new tab, with the same application" + - " and the same storage tree"); + test("Retrieving revision with attachments with removals", + function () { + stop(); + expect(2); + var jio = this.jio, + revision = this.revision, + not_revision = this.not_revision, + timestamps, + blobs1 = [ + new Blob(['a']), + new Blob(['ab']), + new Blob(['abc']), + new Blob(['abcd']), + new Blob(['abcde']) + ]; + jio.put("doc", {title: "bar"}) + .push(function () { + return jio.put("doc", {title: "bar0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[0]); + }) + .push(function () { + return jio.put("doc2", {title: "foo0"}); + }) + .push(function () { + return jio.putAttachment("doc", "data", blobs1[1]); + }) + .push(function () { + return jio.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc", + //timestamp: timestamps[4], + value: {title: "bar0"} + }, + { + doc: {}, + id: "doc2", + //timestamp: timestamps[3], + value: {title: "foo0"} + } + ], + "allDocs with include_revisions false should return all revisions"); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "ascending"]] + }); + }) + .push(function (results) { + timestamps = results.data.rows.map(function (d) { + return d.id; + }); + }) + .push(function () { + return revision.allDocs({ + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: timestamps[4], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[3], + value: {title: "foo0"} + }, + { + doc: {}, + id: timestamps[2], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[1], + value: {title: "bar0"} + }, + { + doc: {}, + id: timestamps[0], + value: {title: "bar"} + } + ], + "allDocs with include_revisions true should return all revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - // Get the document from the first storage - shared.doc._rev = shared.rev; - shared.doc._revisions = {"ids": [shared.hex], "start": 1}; - shared.doc._revs_info = [{"rev": shared.rev, "status": "available"}]; - return jio2.get({"_id": "sample1", "_rev": shared.rev}, { - "revs_info": true, - "revs": true, - "conflicts": true + module("revisionStorage.pack", { + setup: function () { + // create storage of type "revision" with memory as substorage + var dbname = "db_" + Date.now(); + this.jio = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - - }).then(function (answer) { - - deepEqual( - answer.data, - shared.doc, - "And, on this new tab, I load the document, " + - "and my application keeps the revision in memory" - ); - - // MODIFY the 2nd version - shared.doc_2 = {"_id": "sample1", "_rev": shared.rev, - "title": "mySample2_modified"}; - shared.revisions_2 = {"start": 1, "ids": [shared.hex]}; - shared.hex_2 = generateRevisionHash(shared.doc_2, shared.revisions_2); - shared.rev_2 = "2-" + shared.hex_2; - - return jio2.put(shared.doc_2); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "sample1", - "method": "put", - "result": "success", - "rev": shared.rev_2, - "status": 204, - "statusText": "No Content" - }, "So, I can modify and update it"); - - // MODIFY first version - shared.doc_1 = { - "_id": "sample1", - "_rev": shared.rev, - "title": "mySample1_modified" - }; - shared.revisions_1 = {"start": 1, "ids": [shared.rev.split('-')[1]]}; - shared.hex_1 = generateRevisionHash(shared.doc_1, shared.revisions_1); - shared.rev_1 = "2-" + shared.hex_1; - - return jio.put(shared.doc_1); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "sample1", - "method": "put", - "result": "success", - "rev": shared.rev_1, - "status": 204, - "statusText": "No Content" - }, "Back to the first tab, I update the document."); - - // Close 1st tab - jio = undefined; - // Close 2nd tab - jio2 = undefined; - ok(true, "I close tab both tabs"); - - // Reopen JIO - jio = jIO.createJIO(shared.revision_storage_desciption, { - "workspace": shared.workspace1 + this.revision = jIO.createJIO({ + type: "uuid", + sub_storage: { + type: "query", + sub_storage: { + type: "revision", + include_revisions: true, + sub_storage: { + type: "query", + sub_storage: { + type: "indexeddb", + database: dbname + } + } + } + } }); - ok(jio, "Later, I open my application again"); - - // GET document without revision = winner & conflict! - shared.mydocSample3 = { - "_id": "sample1", - "title": "mySample1_modified", - "_rev": shared.rev_1 - }; - shared.mydocSample3._conflicts = [shared.rev_2]; - shared.mydocSample3._revs_info = [{ - "rev": shared.rev_1, - "status": "available" - }, { - "rev": shared.rev, - "status": "available" - }]; - shared.mydocSample3._revisions = { - "ids": [shared.hex_1, shared.hex], - "start": 2 - }; - return jio.get({"_id": "sample1"}, { - "revs_info": true, - "revs": true, - "conflicts": true + this.not_revision = jIO.createJIO({ + type: "query", + sub_storage: { + type: "uuid", + sub_storage: { + type: "indexeddb", + database: dbname + } + } }); + this.blob = new Blob(['a']); + } + }); - }).then(function (answer) { - - deepEqual( - answer.data, - shared.mydocSample3, - "I load the same document as before, " + - "and a popup shows that there is a conflict" - ); - - // REMOVE one of the two conflicting versions - shared.revisions = {"start": 2, "ids": [ - shared.rev_1.split('-')[1], - shared.rev.split('-')[1] - ]}; - shared.doc_myremove3 = {"_id": "sample1", "_rev": shared.rev_1}; - shared.rev_3 = "3-" + generateRevisionHash( - shared.doc_myremove3, - shared.revisions, - true - ); - - return jio.remove({"_id": "sample1", "_rev": shared.rev_1}); - - }).then(function (answer) { - - deepEqual(answer, { - "id": "sample1", - "method": "remove", - "result": "success", - "rev": shared.rev_3, - "status": 204, - "statusText": "No Content" - }, "I choose one of the document and close the application."); - - // check to see if conflict still exists - shared.mydocSample4 = { - "_id": "sample1", - "title": "mySample2_modified", - "_rev": shared.rev_2 - }; - shared.mydocSample4._revs_info = [{ - "rev": shared.rev_2, - "status": "available" - }, { - "rev": shared.rev, - "status": "available" - }]; - shared.mydocSample4._revisions = { - "ids": [shared.hex_2, shared.hex], - "start": 2 - }; - - return jio.get({"_id": "sample1"}, { - "revs_info": true, - "revs": true, - "conflicts": true - }); + test("Verifying pack works with keep_latest_num", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision; + return jio.put("doc_a", {title: "rev"}) + .push(function () { + return jio.put("doc_a", {title: "rev0"}); + }) + .push(function () { + return jio.put("doc_a", {title: "rev1"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data0"}); + }) + .push(function () { + return jio.put("doc_a", {title: "rev2"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data1"}); + }) + .push(function () { + return jio.put("doc_b", {title: "data2"}); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_latest_num: 2 + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 4, "Correct amount of results"); + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + doc: {title: "data2"}, + doc_id: "doc_b", + timestamp: results.data.rows[0].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "data1"}, + doc_id: "doc_b", + timestamp: results.data.rows[1].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[2].id, + value: { + doc: {title: "rev2"}, + doc_id: "doc_a", + timestamp: results.data.rows[2].id, + op: "put" + } + }, + { + doc: {}, + id: results.data.rows[3].id, + value: { + doc: {title: "rev1"}, + doc_id: "doc_a", + timestamp: results.data.rows[3].id, + op: "put" + } + } + ], + "Keep the correct documents after pack"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).then(function (answer) { + test("Verifying pack works with fixed timestamp", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "old_data0"}), + jio.put("doc_b", {title: "old_data1"}), + jio.put("doc_b", {title: "old_data2"}), + jio.put("doc_c", {title: "latest_bar"}) + ]); + }) + .push(function () { + return not_revision.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.put("doc_a", {title: "latest_rev"}); + }) + .push(function () { + return jio.put("doc_b", {title: "latest_data"}); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp"] + }); + }) + .push(function (results) { + equal(results.data.total_rows, 3, "Correct amount of results"); + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_rev"}, + doc_id: "doc_a", + timestamp: results.data.rows[1].id + } + }, + { + doc: {}, + id: results.data.rows[2].id, + value: { + doc: {title: "latest_bar"}, + doc_id: "doc_c", + timestamp: results.data.rows[2].id + } + } + ], + "Keep the correct documents after pack"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - deepEqual( - answer.data, - shared.mydocSample4, - "Test if conflict stiil exists" - ); + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }).fail(unexpectedError).always(start); + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.put("doc_a", {title: "old_rev1"}), + jio.put("doc_a", {title: "old_rev2"}), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); - }); + test("Verifying pack works with fixed timestamp and more complex operations", + function () { + stop(); + expect(2); + var jio = this.jio, + not_revision = this.not_revision, + timestamp, + blob = this.blob; + return jio.allDocs() + .push(function () { + return RSVP.all([ + jio.put("doc_a", {title: "old_rev0"}), + jio.putAttachment("doc_a", "attach_aa", blob), + jio.put("doc_b", {title: "latest_data"}) + ]); + }) + .push(function () { + return jio.allDocs({sort_on: [["timestamp", "descending"]]}); + }) + .push(function (results) { + timestamp = results.data.rows[0].id; + return jio.remove("doc_a"); + }) + .push(function () { + return jio.__storage._sub_storage.__storage._sub_storage + .__storage.packOldRevisions({ + keep_active_revs: timestamp + }); + }) + .push(function () { + return not_revision.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["doc", "doc_id", "timestamp", "op"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: results.data.rows[0].id, + value: { + op: "remove", + doc_id: "doc_a", + timestamp: results.data.rows[0].id + } + }, + { + doc: {}, + id: results.data.rows[1].id, + value: { + doc: {title: "latest_data"}, + doc_id: "doc_b", + op: "put", + timestamp: results.data.rows[1].id + } + } + ], + "Keep the correct documents after pack"); + }) + .push(function () { + return jio.allDocs({ + sort_on: [["timestamp", "descending"]], + select_list: ["title"] + }); + }) + .push(function (results) { + deepEqual(results.data.rows, [ + { + doc: {}, + id: "doc_b", + value: {title: "latest_data"} + } + ], + "Memory not corrupted by pack without include_revisions"); + }) + .fail(function (error) { + //console.log(error); + ok(false, error); + }) + .always(function () {start(); }); + }); -})); +}(jIO, RSVP, Blob, QUnit)); \ No newline at end of file diff --git a/test/tests.html b/test/tests.html index 7b484b12f2b78e7d114c99cf132a9e82a9e75876..65d388f49fb21bfea27c0535afde6a6e342ca24b 100644 --- a/test/tests.html +++ b/test/tests.html @@ -59,14 +59,13 @@ - + +