Commit 1f034e93 authored by Bryan Kaperick's avatar Bryan Kaperick

Changed the name of historystorage.

parent a1d4c506
/*jslint nomen: true*/
/*global RSVP, SimpleQuery, ComplexQuery*/
(function (jIO, RSVP, SimpleQuery, ComplexQuery) {
"use strict";
// Used to distinguish between operations done within the same millisecond
function generateUniqueTimestamp(time) {
// 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;
}
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);
}
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};
}
log = seen[docum.value.doc_id];
log.count += 1;
//log.id = docum.id;
// 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;
}
// 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;
}
if ((docum.op === "putAttachment" || docum.op === "removeAttachment") &&
log.hasOwnProperty(docum.name) &&
!log[docum.name].hasOwnProperty("prA")) {
log[docum.name].prA = ind;
log.final = ind;
}
}
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);
}
function throwCantFindError(id) {
throw new jIO.util.jIOError(
"HistoryStorage: cannot find object '" + id + "'",
404
);
}
function throwRemovedError(id) {
throw new jIO.util.jIOError(
"HistoryStorage: cannot find object '" + id + "' (removed)",
404
);
}
/**
* The jIO HistoryStorage extension
*
* @class HistoryStorage
* @constructor
*/
function HistoryStorage(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;
});
});
};
}
HistoryStorage.prototype.get = function (id_in) {
// Query to get the last edit made to this document
var substorage = this._sub_storage,
doc_id_query,
metadata_query,
options;
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);
});
};
HistoryStorage.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"
};
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);
};
HistoryStorage.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);
};
HistoryStorage.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 RSVP.hash(attachment_promises);
});
};
HistoryStorage.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;
}
});
}
return this._sub_storage.put(timestamp, metadata)
.push(function () {
return substorage.putAttachment(timestamp, name, blob);
});
};
HistoryStorage.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"]
};
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);
}
return substorage.getAttachment(results.data.rows[0].id, name);
}
throwCantFindError(id);
});
};
HistoryStorage.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);
};
HistoryStorage.prototype.repair = function () {
return this._sub_storage.repair.apply(this._sub_storage, arguments);
};
HistoryStorage.prototype.hasCapacity = function (name) {
return name === 'list' ||
name === 'include' ||
name === 'query' ||
name === 'select';
};
HistoryStorage.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;
}
if (docum.value.op === "remove") {
docum.value.doc = {};
return docum;
}
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;
}
}
}
}
return false;
});
} else {
// 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;
}
} 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 = {};
}
}
}
}
return false;
});
}
docs_to_query = results
// Filter out all docs flagged as false in previous map call
.filter(function (docum) {
return docum;
})
// Put into correct format to be passed back to query storage
.map(function (docum) {
if (include_revs) {
docum.id = docum.value.timestamp;
//docum.doc = docum.value.doc;
} else {
docum.id = docum.value.doc_id;
//docum.doc = docum.value.doc;
}
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;
});
};
jIO.addStorage('history', HistoryStorage);
}(jIO, RSVP, SimpleQuery, ComplexQuery));
\ No newline at end of file
/*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": <sub storage description>
* }
*/
// 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
/*jslint nomen: true*/
/*global Blob*/
(function (jIO, RSVP, Blob, QUnit) {
"use strict";
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
);
});
}
module("HistoryStorage.post", {
setup: function () {
// create storage of type "history" with memory as substorage
var dbname = "db_" + Date.now();
this.jio = jIO.createJIO({
type: "uuid",
sub_storage: {
type: "query",
sub_storage: {
type: "history",
sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.history = jIO.createJIO({
type: "uuid",
sub_storage: {
type: "query",
sub_storage: {
type: "history",
include_revisions: true,
sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.not_history = jIO.createJIO({
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
});
}
});
test("Verifying simple post works",
function () {
stop();
expect(2);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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"
}, "history storage only retrieves latest version");
})
.push(function () {
return not_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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(); });
});
/////////////////////////////////////////////////////////////////
// Attachments
/////////////////////////////////////////////////////////////////
module("HistoryStorage.attachments", {
setup: function () {
// create storage of type "history" 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: "history",
sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.history = jIO.createJIO({
type: "query",
sub_storage: {
type: "history",
include_revisions: true,
sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.not_history = jIO.createJIO({
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
});
}
});
test("Testing proper adding/removing attachments",
function () {
stop();
expect(10);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.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 history.getAttachment(
timestamps[3],
"attacheddata"
);
})
.push(function (result) {
deepEqual(result,
blob2,
"Return the attachment information with getAttachment for " +
"current revision"
);
return history.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,
"HistoryStorage: cannot find object '" + timestamps[0] + "'",
"Error caught by history 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(); });
});
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,
"HistoryStorage: cannot find object 'doc' (removed)",
"Error is handled by Historystorage.");
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
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(); });
});
test("Correctness of allAttachments method on current attachments",
function () {
stop();
expect(14);
var jio = this.jio,
not_history = this.not_history,
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_history.allDocs();
})
.push(function (results) {
var promises = results.data.rows.map(function (data) {
return not_history.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_history.allDocs();
})
.push(function (results) {
return RSVP.all(results.data.rows.map(function (d) {
return not_history.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,
"HistoryStorage: cannot find object 'doc' (removed)",
"Error is handled by Historystorage.");
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
test("Correctness of allAttachments method on older revisions",
function () {
stop();
expect(11);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.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 history.allAttachments(timestamps[0]);
})
.push(function (results) {
deepEqual(results, {}, "First version of document has 0 attachments");
return history.allAttachments(timestamps[1]);
})
.push(function (results) {
deepEqual(results, {
data: blob1,
data2: blob2
}, "Both attachments are included in allAttachments");
return history.allAttachments(timestamps[2]);
})
.push(function (results) {
deepEqual(results, {
data: blob1
}, "Removed attachment does not show up in allAttachments");
return history.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,
"HistoryStorage: cannot find object '" + timestamps[3] +
"' (removed)",
"Error is handled by Historystorage.");
})
.push(function () {
return history.allAttachments(timestamps[4]);
})
.push(function (results) {
deepEqual(results, {
data: blob11
});
})
.push(function () {
return history.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,
"HistoryStorage: cannot find object 'not-a-timestamp-or-doc_id'",
"Error is handled by Historystorage.");
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
/////////////////////////////////////////////////////////////////
// Querying older revisions
/////////////////////////////////////////////////////////////////
module("HistoryStorage.get", {
setup: function () {
// create storage of type "history" with memory as substorage
var dbname = "db_" + Date.now();
this.jio = jIO.createJIO({
type: "query",
sub_storage: {
type: "history",
sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.history = jIO.createJIO({
type: "query",
sub_storage: {
type: "history",
include_revisions: true,
sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.not_history = jIO.createJIO({
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
});
}
});
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,
"HistoryStorage: cannot find object 'doc' (removed)",
"Error is handled by history 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(); });
});
test("Removing documents and then putting them",
function () {
stop();
expect(2);
var jio = this.jio,
history = this.history,
timestamps,
not_history = this.not_history;
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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,
history = this.history,
not_history = this.not_history,
timestamp;
jio.put("doc", {title: "foo"})
.push(function () {
return not_history.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 history.get(timestamp);
})
.push(function (result) {
deepEqual(result, {
title: "foo"
}, "Saving document with timestamp id does not cause issues (2)");
return history.get(timestamp);
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
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,
"HistoryStorage: cannot find object 'doc'",
"Error is handled by history storage before reaching console");
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
test("Getting a document with timestamp when include_revisions is false",
function () {
stop();
expect(6);
var jio = this.jio,
not_history = this.not_history,
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,
"HistoryStorage: cannot find object 'doc'",
"Error is handled by history storage before reaching console");
})
.push(function () {
return not_history.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,
"HistoryStorage: cannot find object '" + timestamp + "'",
"Error is handled by history storage before reaching console");
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
test("Creating a document with put and retrieving it with get",
function () {
stop();
expect(5);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
timestamps;
jio.put("doc", {title: "version0"})
.push(function () {
return not_history.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 history.get(timestamps[0]);
})
.push(function (result) {
deepEqual(result, {
title: "version0"
}, "Get document from history storage");
return not_history.get(
timestamps[0]
);
})
.push(function (result) {
deepEqual(result, {
timestamp: timestamps[0],
op: "put",
doc_id: "doc",
doc: {
title: "version0"
}
}, "Get document from non-history 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(); });
});
test("Updating a document with include revisions",
function () {
stop();
expect(1);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
timestamps,
t_id;
jio.put("doc", {title: "version0"})
.push(function () {
return history.put("doc", {title: "version1"});
})
.push(function () {
return not_history.allDocs({sort_on: [["timestamp", "ascending"]]});
})
.push(function (results) {
t_id = results.data.rows[0].id;
return history.put(t_id, {title: "version0.1"});
})
.push(function () {
return jio.put(t_id, {title: "label0"});
})
.push(function () {
return history.put("1234567891012-abcd", {k: "v"});
})
.push(function () {
return not_history.allDocs({
select_list: ["timestamp"]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.value.timestamp;
});
})
.push(function () {
return not_history.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(); });
});
test("Retrieving older revisions with get",
function () {
stop();
expect(7);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.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 history.get(timestamps[0]);
}, function (err) {
ok(false, err);
})
.push(function (result) {
deepEqual(result, {
title: "t0",
subtitle: "s0"
}, "Get returns first version");
return history.get(timestamps[1]);
})
.push(function (result) {
deepEqual(result, {
title: "t1",
subtitle: "s1"
}, "Get returns second version");
return history.get(timestamps[2]);
}, function (err) {
ok(false, err);
})
.push(function (result) {
deepEqual(result, {
title: "t2",
subtitle: "s2"
}, "Get returns third version");
return history.get(timestamps[3]);
}, function (err) {
ok(false, err);
})
.push(function () {
ok(false, "This should have thrown a 404 error");
return history.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 history.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(); });
});
test("verifying updates correctly when puts are done in parallel",
function () {
stop();
expect(8);
var jio = this.jio,
not_history = this.not_history;
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_history.allDocs({
query: "",
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
equal(results.data.total_rows,
9,
"All nine versions exist in storage");
return not_history.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_history.allDocs({
query: "",
sort_on: [["op", "descending"]]
});
})
.push(function (results) {
equal(results.data.total_rows,
10,
"Remove operation is recorded");
return not_history.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(); });
});
test("Getting after attachments have been put",
function () {
stop();
expect(4);
var jio = this.jio,
not_history = this.not_history,
history = this.history,
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_history.allDocs({select_list: ["title"]});
})
.push(function (results) {
edit_log = results.data.rows;
return history.get(edit_log[0].id);
})
.push(function (result) {
deepEqual(result, {title: "foo0"});
return history.get(edit_log[1].id);
})
.push(function (result) {
deepEqual(result, {title: "foo0"});
return history.get(edit_log[2].id);
})
.push(function (result) {
deepEqual(result, {title: "foo0"});
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
/////////////////////////////////////////////////////////////////
// Querying older revisions
/////////////////////////////////////////////////////////////////
module("HistoryStorage.allDocs", {
setup: function () {
// create storage of type "history" with memory as substorage
this.dbname = "db_" + Date.now();
this.jio = jIO.createJIO({
type: "uuid",
sub_storage: {
type: "query",
sub_storage: {
type: "history",
sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: this.dbname
}
}
}
}
});
this.history = jIO.createJIO({
type: "uuid",
sub_storage: {
type: "query",
sub_storage: {
type: "history",
include_revisions: true,
sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: this.dbname
}
}
}
}
});
this.not_history = jIO.createJIO({
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: this.dbname
}
}
});
}
});
test("Putting a document and retrieving it with allDocs",
function () {
stop();
expect(7);
var jio = this.jio,
not_history = this.not_history,
timestamp;
jio.put("doc", {title: "version0"})
.push(function () {
return not_history.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_history.allDocs();
})
.push(function (results) {
timestamp = results.data.rows[0].id;
equal(results.data.total_rows,
1,
"Exactly one result returned");
return not_history.get(timestamp);
})
.push(function (result) {
deepEqual(result, {
doc_id: "doc",
doc: {
title: "version0"
},
timestamp: timestamp,
op: "put"
},
"When a different type of storage queries historystorage, all " +
"metadata is returned correctly"
);
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
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(); });
});
test("Putting a document, revising it, and retrieving revisions with allDocs",
function () {
stop();
expect(10);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.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 history.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 history is included.");
return not_history.allDocs({
sort_on: [["title", "ascending"]]
});
})
.push(function (results) {
return RSVP.all(results.data.rows.map(function (d) {
return not_history.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(); });
});
test(
"Putting and removing documents, latest revisions and no removed documents",
function () {
stop();
expect(3);
var jio = this.jio,
not_history = this.not_history,
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_history.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(); });
});
/////////////////////////////////////////////////////////////////
// Complex Queries with Revision Querying
/////////////////////////////////////////////////////////////////
test("More complex query with different options (with revision queries)",
function () {
stop();
expect(3);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return not_history.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_history.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 history.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(); });
});
test(
"allDocs with include_revisions with an attachment on a removed document",
function () {
stop();
expect(1);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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_history.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,
history = this.history,
blob = new Blob(['a']),
timestamps,
not_history = this.not_history;
jio.put("document", {title: "foo"})
.push(function () {
return jio.putAttachment("document", "attachment", blob);
})
.push(function () {
return jio.removeAttachment("document", "attachment");
})
.push(function () {
return not_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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(); });
});
test("allDocs with include_revisions only one document",
function () {
stop();
expect(1);
var jio = this.jio,
history = this.history,
timestamps,
not_history = this.not_history;
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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 history is returned"
);
})
.fail(function (error) {
//console.log(error);
ok(false, error);
})
.always(function () {start(); });
});
test("Parallel edits will not break anything",
function () {
stop();
expect(2);
var jio = this.jio,
history = this.history,
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 history.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(); });
});
test("Adding second query storage on top of history",
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(); });
});
module("HistoryStorage.Full-Example", {
setup: function () {
// create storage of type "history" 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: "history",
sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.history = jIO.createJIO({
type: "query",
sub_storage: {
type: "history",
include_revisions: true,
sub_storage: {
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.not_history = jIO.createJIO({
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
});
}
});
test("Retrieving history with attachments",
function () {
stop();
expect(1);
var jio = this.jio,
history = this.history,
timestamps,
not_history = this.not_history,
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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(); });
});
test("Retrieving history with attachments with less straightforward ordering",
function () {
stop();
expect(1);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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(); });
});
test("Retrieving history with attachments with removals",
function () {
stop();
expect(2);
var jio = this.jio,
history = this.history,
not_history = this.not_history,
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_history.allDocs({
sort_on: [["timestamp", "ascending"]]
});
})
.push(function (results) {
timestamps = results.data.rows.map(function (d) {
return d.id;
});
})
.push(function () {
return history.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(); });
});
module("HistoryStorage.pack", {
setup: function () {
// create storage of type "history" with memory as substorage
var dbname = "db_" + Date.now();
this.jio = jIO.createJIO({
type: "uuid",
sub_storage: {
type: "query",
sub_storage: {
type: "history",
sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.history = jIO.createJIO({
type: "uuid",
sub_storage: {
type: "query",
sub_storage: {
type: "history",
include_revisions: true,
sub_storage: {
type: "query",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
}
}
});
this.not_history = jIO.createJIO({
type: "query",
sub_storage: {
type: "uuid",
sub_storage: {
type: "indexeddb",
database: dbname
}
}
});
this.blob = new Blob(['a']);
}
});
test("Verifying pack works with keep_latest_num",
function () {
stop();
expect(2);
var jio = this.jio,
not_history = this.not_history;
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_history.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(); });
});
test("Verifying pack works with fixed timestamp",
function () {
stop();
expect(2);
var jio = this.jio,
not_history = this.not_history,
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_history.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_history.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(); });
});
test("Verifying pack works with fixed timestamp and more complex operations",
function () {
stop();
expect(2);
var jio = this.jio,
not_history = this.not_history,
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_history.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_history = this.not_history,
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_history.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_history = this.not_history,
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_history.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
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment