pouchdb-adapter-utils
Version:
Utilities for PouchDB adapters.
529 lines (465 loc) • 15.5 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
var pouchdbUtils = require('pouchdb-utils');
var pouchdbBinaryUtils = require('pouchdb-binary-utils');
var pouchdbMd5 = require('pouchdb-md5');
var pouchdbErrors = require('pouchdb-errors');
var pouchdbMerge = require('pouchdb-merge');
function allDocsKeysQuery(api, opts) {
var keys = opts.keys;
var finalResults = {
offset: opts.skip
};
return Promise.all(keys.map(function (key) {
var subOpts = Object.assign({key, deleted: 'ok'}, opts);
['limit', 'skip', 'keys'].forEach(function (optKey) {
delete subOpts[optKey];
});
return new Promise(function (resolve, reject) {
api._allDocs(subOpts, function (err, res) {
/* istanbul ignore if */
if (err) {
return reject(err);
}
/* istanbul ignore if */
if (opts.update_seq && res.update_seq !== undefined) {
finalResults.update_seq = res.update_seq;
}
finalResults.total_rows = res.total_rows;
resolve(res.rows[0] || {key, error: 'not_found'});
});
});
})).then(function (results) {
finalResults.rows = results;
return finalResults;
});
}
//
// Blobs are not supported in all versions of IndexedDB, notably
// Chrome <37, Android <5 and (some?) webkit-based browsers.
// In those versions, storing a blob will throw.
//
// Example Webkit error:
// > DataCloneError: Failed to store record in an IDBObjectStore: BlobURLs are not yet supported.
//
// Various other blob bugs exist in Chrome v37-42 (inclusive).
// Detecting them is expensive and confusing to users, and Chrome 37-42
// is at very low usage worldwide, so we do a hacky userAgent check instead.
//
// content-type bug: https://code.google.com/p/chromium/issues/detail?id=408120
// 404 bug: https://code.google.com/p/chromium/issues/detail?id=447916
// FileReader bug: https://code.google.com/p/chromium/issues/detail?id=447836
//
function checkBlobSupport(txn, store, docIdOrCreateDoc) {
return new Promise(function (resolve) {
var blob = pouchdbBinaryUtils.blob(['']);
let req;
if (typeof docIdOrCreateDoc === 'function') {
// Store may require a specific key path, in which case we can't store the
// blob directly in the store.
const createDoc = docIdOrCreateDoc;
const doc = createDoc(blob);
req = txn.objectStore(store).put(doc);
} else {
const docId = docIdOrCreateDoc;
req = txn.objectStore(store).put(blob, docId);
}
req.onsuccess = function () {
var matchedChrome = navigator.userAgent.match(/Chrome\/(\d+)/);
var matchedEdge = navigator.userAgent.match(/Edge\//);
// MS Edge pretends to be Chrome 42:
// https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx
resolve(matchedEdge || !matchedChrome ||
parseInt(matchedChrome[1], 10) >= 43);
};
req.onerror = txn.onabort = function (e) {
// If the transaction aborts now its due to not being able to
// write to the database, likely due to the disk being full
e.preventDefault();
e.stopPropagation();
resolve(false);
};
}).catch(function () {
return false; // error, so assume unsupported
});
}
function toObject(array) {
return array.reduce(function (obj, item) {
obj[item] = true;
return obj;
}, {});
}
// List of top level reserved words for doc
var reservedWords = toObject([
'_id',
'_rev',
'_access',
'_attachments',
'_deleted',
'_revisions',
'_revs_info',
'_conflicts',
'_deleted_conflicts',
'_local_seq',
'_rev_tree',
// replication documents
'_replication_id',
'_replication_state',
'_replication_state_time',
'_replication_state_reason',
'_replication_stats',
// Specific to Couchbase Sync Gateway
'_removed'
]);
// List of reserved words that should end up in the document
var dataWords = toObject([
'_access',
'_attachments',
// replication documents
'_replication_id',
'_replication_state',
'_replication_state_time',
'_replication_state_reason',
'_replication_stats'
]);
function parseRevisionInfo(rev) {
if (!/^\d+-/.test(rev)) {
return pouchdbErrors.createError(pouchdbErrors.INVALID_REV);
}
var idx = rev.indexOf('-');
var left = rev.substring(0, idx);
var right = rev.substring(idx + 1);
return {
prefix: parseInt(left, 10),
id: right
};
}
function makeRevTreeFromRevisions(revisions, opts) {
var pos = revisions.start - revisions.ids.length + 1;
var revisionIds = revisions.ids;
var ids = [revisionIds[0], opts, []];
for (var i = 1, len = revisionIds.length; i < len; i++) {
ids = [revisionIds[i], {status: 'missing'}, [ids]];
}
return [{
pos,
ids
}];
}
// Preprocess documents, parse their revisions, assign an id and a
// revision for new writes that are missing them, etc
function parseDoc(doc, newEdits, dbOpts) {
if (!dbOpts) {
dbOpts = {
deterministic_revs: true
};
}
var nRevNum;
var newRevId;
var revInfo;
var opts = {status: 'available'};
if (doc._deleted) {
opts.deleted = true;
}
if (newEdits) {
if (!doc._id) {
doc._id = pouchdbUtils.uuid();
}
newRevId = pouchdbUtils.rev(doc, dbOpts.deterministic_revs);
if (doc._rev) {
revInfo = parseRevisionInfo(doc._rev);
if (revInfo.error) {
return revInfo;
}
doc._rev_tree = [{
pos: revInfo.prefix,
ids: [revInfo.id, {status: 'missing'}, [[newRevId, opts, []]]]
}];
nRevNum = revInfo.prefix + 1;
} else {
doc._rev_tree = [{
pos: 1,
ids : [newRevId, opts, []]
}];
nRevNum = 1;
}
} else {
if (doc._revisions) {
doc._rev_tree = makeRevTreeFromRevisions(doc._revisions, opts);
nRevNum = doc._revisions.start;
newRevId = doc._revisions.ids[0];
}
if (!doc._rev_tree) {
revInfo = parseRevisionInfo(doc._rev);
if (revInfo.error) {
return revInfo;
}
nRevNum = revInfo.prefix;
newRevId = revInfo.id;
doc._rev_tree = [{
pos: nRevNum,
ids: [newRevId, opts, []]
}];
}
}
pouchdbUtils.invalidIdError(doc._id);
doc._rev = nRevNum + '-' + newRevId;
var result = {metadata : {}, data : {}};
for (var key in doc) {
/* istanbul ignore else */
if (Object.prototype.hasOwnProperty.call(doc, key)) {
var specialKey = key[0] === '_';
if (specialKey && !reservedWords[key]) {
var error = pouchdbErrors.createError(pouchdbErrors.DOC_VALIDATION, key);
error.message = pouchdbErrors.DOC_VALIDATION.message + ': ' + key;
throw error;
} else if (specialKey && !dataWords[key]) {
result.metadata[key.slice(1)] = doc[key];
} else {
result.data[key] = doc[key];
}
}
}
return result;
}
function parseBase64(data) {
try {
return pouchdbBinaryUtils.atob(data);
} catch (e) {
var err = pouchdbErrors.createError(pouchdbErrors.BAD_ARG,
'Attachment is not a valid base64 string');
return {error: err};
}
}
function preprocessString(att, blobType, callback) {
var asBinary = parseBase64(att.data);
if (asBinary.error) {
return callback(asBinary.error);
}
att.length = asBinary.length;
if (blobType === 'blob') {
att.data = pouchdbBinaryUtils.binaryStringToBlobOrBuffer(asBinary, att.content_type);
} else if (blobType === 'base64') {
att.data = pouchdbBinaryUtils.btoa(asBinary);
} else { // binary
att.data = asBinary;
}
pouchdbMd5.binaryMd5(asBinary, function (result) {
att.digest = 'md5-' + result;
callback();
});
}
function preprocessBlob(att, blobType, callback) {
pouchdbMd5.binaryMd5(att.data, function (md5) {
att.digest = 'md5-' + md5;
// size is for blobs (browser), length is for buffers (node)
att.length = att.data.size || att.data.length || 0;
if (blobType === 'binary') {
pouchdbBinaryUtils.blobOrBufferToBinaryString(att.data, function (binString) {
att.data = binString;
callback();
});
} else if (blobType === 'base64') {
pouchdbBinaryUtils.blobOrBufferToBase64(att.data, function (b64) {
att.data = b64;
callback();
});
} else {
callback();
}
});
}
function preprocessAttachment(att, blobType, callback) {
if (att.stub) {
return callback();
}
if (typeof att.data === 'string') { // input is a base64 string
preprocessString(att, blobType, callback);
} else { // input is a blob
preprocessBlob(att, blobType, callback);
}
}
function preprocessAttachments(docInfos, blobType, callback) {
if (!docInfos.length) {
return callback();
}
var docv = 0;
var overallErr;
docInfos.forEach(function (docInfo) {
var attachments = docInfo.data && docInfo.data._attachments ?
Object.keys(docInfo.data._attachments) : [];
var recv = 0;
if (!attachments.length) {
return done();
}
function processedAttachment(err) {
overallErr = err;
recv++;
if (recv === attachments.length) {
done();
}
}
for (var key in docInfo.data._attachments) {
if (Object.prototype.hasOwnProperty.call(docInfo.data._attachments, key)) {
preprocessAttachment(docInfo.data._attachments[key],
blobType, processedAttachment);
}
}
});
function done() {
docv++;
if (docInfos.length === docv) {
if (overallErr) {
callback(overallErr);
} else {
callback();
}
}
}
}
function updateDoc(revLimit, prev, docInfo, results,
i, cb, writeDoc, newEdits) {
if (pouchdbMerge.revExists(prev.rev_tree, docInfo.metadata.rev) && !newEdits) {
results[i] = docInfo;
return cb();
}
// sometimes this is pre-calculated. historically not always
var previousWinningRev = prev.winningRev || pouchdbMerge.winningRev(prev);
var previouslyDeleted = 'deleted' in prev ? prev.deleted :
pouchdbMerge.isDeleted(prev, previousWinningRev);
var deleted = 'deleted' in docInfo.metadata ? docInfo.metadata.deleted :
pouchdbMerge.isDeleted(docInfo.metadata);
var isRoot = /^1-/.test(docInfo.metadata.rev);
if (previouslyDeleted && !deleted && newEdits && isRoot) {
var newDoc = docInfo.data;
newDoc._rev = previousWinningRev;
newDoc._id = docInfo.metadata.id;
docInfo = parseDoc(newDoc, newEdits);
}
var merged = pouchdbMerge.merge(prev.rev_tree, docInfo.metadata.rev_tree[0], revLimit);
var inConflict = newEdits && ((
(previouslyDeleted && deleted && merged.conflicts !== 'new_leaf') ||
(!previouslyDeleted && merged.conflicts !== 'new_leaf') ||
(previouslyDeleted && !deleted && merged.conflicts === 'new_branch')));
if (inConflict) {
var err = pouchdbErrors.createError(pouchdbErrors.REV_CONFLICT);
results[i] = err;
return cb();
}
var newRev = docInfo.metadata.rev;
docInfo.metadata.rev_tree = merged.tree;
docInfo.stemmedRevs = merged.stemmedRevs || [];
/* istanbul ignore else */
if (prev.rev_map) {
docInfo.metadata.rev_map = prev.rev_map; // used only by leveldb
}
// recalculate
var winningRev = pouchdbMerge.winningRev(docInfo.metadata);
var winningRevIsDeleted = pouchdbMerge.isDeleted(docInfo.metadata, winningRev);
// calculate the total number of documents that were added/removed,
// from the perspective of total_rows/doc_count
var delta = (previouslyDeleted === winningRevIsDeleted) ? 0 :
previouslyDeleted < winningRevIsDeleted ? -1 : 1;
var newRevIsDeleted;
if (newRev === winningRev) {
// if the new rev is the same as the winning rev, we can reuse that value
newRevIsDeleted = winningRevIsDeleted;
} else {
// if they're not the same, then we need to recalculate
newRevIsDeleted = pouchdbMerge.isDeleted(docInfo.metadata, newRev);
}
writeDoc(docInfo, winningRev, winningRevIsDeleted, newRevIsDeleted,
true, delta, i, cb);
}
function rootIsMissing(docInfo) {
return docInfo.metadata.rev_tree[0].ids[1].status === 'missing';
}
function processDocs(revLimit, docInfos, api, fetchedDocs, tx, results,
writeDoc, opts, overallCallback) {
// Default to 1000 locally
revLimit = revLimit || 1000;
function insertDoc(docInfo, resultsIdx, callback) {
// Cant insert new deleted documents
var winningRev = pouchdbMerge.winningRev(docInfo.metadata);
var deleted = pouchdbMerge.isDeleted(docInfo.metadata, winningRev);
if ('was_delete' in opts && deleted) {
results[resultsIdx] = pouchdbErrors.createError(pouchdbErrors.MISSING_DOC, 'deleted');
return callback();
}
// 4712 - detect whether a new document was inserted with a _rev
var inConflict = newEdits && rootIsMissing(docInfo);
if (inConflict) {
var err = pouchdbErrors.createError(pouchdbErrors.REV_CONFLICT);
results[resultsIdx] = err;
return callback();
}
var delta = deleted ? 0 : 1;
writeDoc(docInfo, winningRev, deleted, deleted, false,
delta, resultsIdx, callback);
}
var newEdits = opts.new_edits;
var idsToDocs = new Map();
var docsDone = 0;
var docsToDo = docInfos.length;
function checkAllDocsDone() {
if (++docsDone === docsToDo && overallCallback) {
overallCallback();
}
}
docInfos.forEach(function (currentDoc, resultsIdx) {
if (currentDoc._id && pouchdbMerge.isLocalId(currentDoc._id)) {
var fun = currentDoc._deleted ? '_removeLocal' : '_putLocal';
api[fun](currentDoc, {ctx: tx}, function (err, res) {
results[resultsIdx] = err || res;
checkAllDocsDone();
});
return;
}
var id = currentDoc.metadata.id;
if (idsToDocs.has(id)) {
docsToDo--; // duplicate
idsToDocs.get(id).push([currentDoc, resultsIdx]);
} else {
idsToDocs.set(id, [[currentDoc, resultsIdx]]);
}
});
// in the case of new_edits, the user can provide multiple docs
// with the same id. these need to be processed sequentially
idsToDocs.forEach(function (docs, id) {
var numDone = 0;
function docWritten() {
if (++numDone < docs.length) {
nextDoc();
} else {
checkAllDocsDone();
}
}
function nextDoc() {
var value = docs[numDone];
var currentDoc = value[0];
var resultsIdx = value[1];
if (fetchedDocs.has(id)) {
updateDoc(revLimit, fetchedDocs.get(id), currentDoc, results,
resultsIdx, docWritten, writeDoc, newEdits);
} else {
// Ensure stemming applies to new writes as well
var merged = pouchdbMerge.merge([], currentDoc.metadata.rev_tree[0], revLimit);
currentDoc.metadata.rev_tree = merged.tree;
currentDoc.stemmedRevs = merged.stemmedRevs || [];
insertDoc(currentDoc, resultsIdx, docWritten);
}
}
nextDoc();
});
}
exports.invalidIdError = pouchdbUtils.invalidIdError;
exports.normalizeDdocFunctionName = pouchdbUtils.normalizeDdocFunctionName;
exports.parseDdocFunctionName = pouchdbUtils.parseDdocFunctionName;
exports.isDeleted = pouchdbMerge.isDeleted;
exports.isLocalId = pouchdbMerge.isLocalId;
exports.allDocsKeysQuery = allDocsKeysQuery;
exports.checkBlobSupport = checkBlobSupport;
exports.parseDoc = parseDoc;
exports.preprocessAttachments = preprocessAttachments;
exports.processDocs = processDocs;
exports.updateDoc = updateDoc;
;