UNPKG

pouchdb-adapter-utils

Version:

Utilities for PouchDB adapters.

529 lines (465 loc) 15.5 kB
'use strict'; 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;