| import { |
| flatten, |
| guardedConsole, |
| nextTick, |
| isRemote |
| } from 'pouchdb-utils'; |
| |
| import { |
| base64StringToBlobOrBuffer as b64ToBluffer |
| } from 'pouchdb-binary-utils'; |
| |
| import { |
| collate, |
| toIndexableString, |
| normalizeKey, |
| parseIndexableString |
| } from 'pouchdb-collate'; |
| |
| import TaskQueue from './taskqueue'; |
| import createView from './createView'; |
| import { |
| callbackify, |
| sequentialize, |
| uniq, |
| fin, |
| promisedCallback, |
| mapToKeysArray, |
| QueryParseError, |
| NotFoundError, |
| BuiltInError |
| } from 'pouchdb-mapreduce-utils'; |
| |
| var persistentQueues = {}; |
| var tempViewQueue = new TaskQueue(); |
| var CHANGES_BATCH_SIZE = 50; |
| |
| function parseViewName(name) { |
| // can be either 'ddocname/viewname' or just 'viewname' |
| // (where the ddoc name is the same) |
| return name.indexOf('/') === -1 ? [name, name] : name.split('/'); |
| } |
| |
| function isGenOne(changes) { |
| // only return true if the current change is 1- |
| // and there are no other leafs |
| return changes.length === 1 && /^1-/.test(changes[0].rev); |
| } |
| |
| function emitError(db, e) { |
| try { |
| db.emit('error', e); |
| } catch (err) { |
| guardedConsole('error', |
| 'The user\'s map/reduce function threw an uncaught error.\n' + |
| 'You can debug this error by doing:\n' + |
| 'myDatabase.on(\'error\', function (err) { debugger; });\n' + |
| 'Please double-check your map/reduce function.'); |
| guardedConsole('error', e); |
| } |
| } |
| |
| /** |
| * Returns an "abstract" mapreduce object of the form: |
| * |
| * { |
| * query: queryFun, |
| * viewCleanup: viewCleanupFun |
| * } |
| * |
| * Arguments are: |
| * |
| * localDoc: string |
| * This is for the local doc that gets saved in order to track the |
| * "dependent" DBs and clean them up for viewCleanup. It should be |
| * unique, so that indexer plugins don't collide with each other. |
| * mapper: function (mapFunDef, emit) |
| * Returns a map function based on the mapFunDef, which in the case of |
| * normal map/reduce is just the de-stringified function, but may be |
| * something else, such as an object in the case of pouchdb-find. |
| * reducer: function (reduceFunDef) |
| * Ditto, but for reducing. Modules don't have to support reducing |
| * (e.g. pouchdb-find). |
| * ddocValidator: function (ddoc, viewName) |
| * Throws an error if the ddoc or viewName is not valid. |
| * This could be a way to communicate to the user that the configuration for the |
| * indexer is invalid. |
| */ |
| function createAbstractMapReduce(localDocName, mapper, reducer, ddocValidator) { |
| |
| function tryMap(db, fun, doc) { |
| // emit an event if there was an error thrown by a map function. |
| // putting try/catches in a single function also avoids deoptimizations. |
| try { |
| fun(doc); |
| } catch (e) { |
| emitError(db, e); |
| } |
| } |
| |
| function tryReduce(db, fun, keys, values, rereduce) { |
| // same as above, but returning the result or an error. there are two separate |
| // functions to avoid extra memory allocations since the tryCode() case is used |
| // for custom map functions (common) vs this function, which is only used for |
| // custom reduce functions (rare) |
| try { |
| return {output : fun(keys, values, rereduce)}; |
| } catch (e) { |
| emitError(db, e); |
| return {error: e}; |
| } |
| } |
| |
| function sortByKeyThenValue(x, y) { |
| var keyCompare = collate(x.key, y.key); |
| return keyCompare !== 0 ? keyCompare : collate(x.value, y.value); |
| } |
| |
| function sliceResults(results, limit, skip) { |
| skip = skip || 0; |
| if (typeof limit === 'number') { |
| return results.slice(skip, limit + skip); |
| } else if (skip > 0) { |
| return results.slice(skip); |
| } |
| return results; |
| } |
| |
| function rowToDocId(row) { |
| var val = row.value; |
| // Users can explicitly specify a joined doc _id, or it |
| // defaults to the doc _id that emitted the key/value. |
| var docId = (val && typeof val === 'object' && val._id) || row.id; |
| return docId; |
| } |
| |
| function readAttachmentsAsBlobOrBuffer(res) { |
| res.rows.forEach(function (row) { |
| var atts = row.doc && row.doc._attachments; |
| if (!atts) { |
| return; |
| } |
| Object.keys(atts).forEach(function (filename) { |
| var att = atts[filename]; |
| atts[filename].data = b64ToBluffer(att.data, att.content_type); |
| }); |
| }); |
| } |
| |
| function postprocessAttachments(opts) { |
| return function (res) { |
| if (opts.include_docs && opts.attachments && opts.binary) { |
| readAttachmentsAsBlobOrBuffer(res); |
| } |
| return res; |
| }; |
| } |
| |
| function addHttpParam(paramName, opts, params, asJson) { |
| // add an http param from opts to params, optionally json-encoded |
| var val = opts[paramName]; |
| if (typeof val !== 'undefined') { |
| if (asJson) { |
| val = encodeURIComponent(JSON.stringify(val)); |
| } |
| params.push(paramName + '=' + val); |
| } |
| } |
| |
| function coerceInteger(integerCandidate) { |
| if (typeof integerCandidate !== 'undefined') { |
| var asNumber = Number(integerCandidate); |
| // prevents e.g. '1foo' or '1.1' being coerced to 1 |
| if (!isNaN(asNumber) && asNumber === parseInt(integerCandidate, 10)) { |
| return asNumber; |
| } else { |
| return integerCandidate; |
| } |
| } |
| } |
| |
| function coerceOptions(opts) { |
| opts.group_level = coerceInteger(opts.group_level); |
| opts.limit = coerceInteger(opts.limit); |
| opts.skip = coerceInteger(opts.skip); |
| return opts; |
| } |
| |
| function checkPositiveInteger(number) { |
| if (number) { |
| if (typeof number !== 'number') { |
| return new QueryParseError('Invalid value for integer: "' + |
| number + '"'); |
| } |
| if (number < 0) { |
| return new QueryParseError('Invalid value for positive integer: ' + |
| '"' + number + '"'); |
| } |
| } |
| } |
| |
| function checkQueryParseError(options, fun) { |
| var startkeyName = options.descending ? 'endkey' : 'startkey'; |
| var endkeyName = options.descending ? 'startkey' : 'endkey'; |
| |
| if (typeof options[startkeyName] !== 'undefined' && |
| typeof options[endkeyName] !== 'undefined' && |
| collate(options[startkeyName], options[endkeyName]) > 0) { |
| throw new QueryParseError('No rows can match your key range, ' + |
| 'reverse your start_key and end_key or set {descending : true}'); |
| } else if (fun.reduce && options.reduce !== false) { |
| if (options.include_docs) { |
| throw new QueryParseError('{include_docs:true} is invalid for reduce'); |
| } else if (options.keys && options.keys.length > 1 && |
| !options.group && !options.group_level) { |
| throw new QueryParseError('Multi-key fetches for reduce views must use ' + |
| '{group: true}'); |
| } |
| } |
| ['group_level', 'limit', 'skip'].forEach(function (optionName) { |
| var error = checkPositiveInteger(options[optionName]); |
| if (error) { |
| throw error; |
| } |
| }); |
| } |
| |
| function httpQuery(db, fun, opts) { |
| // List of parameters to add to the PUT request |
| var params = []; |
| var body; |
| var method = 'GET'; |
| |
| // If opts.reduce exists and is defined, then add it to the list |
| // of parameters. |
| // If reduce=false then the results are that of only the map function |
| // not the final result of map and reduce. |
| addHttpParam('reduce', opts, params); |
| addHttpParam('include_docs', opts, params); |
| addHttpParam('attachments', opts, params); |
| addHttpParam('limit', opts, params); |
| addHttpParam('descending', opts, params); |
| addHttpParam('group', opts, params); |
| addHttpParam('group_level', opts, params); |
| addHttpParam('skip', opts, params); |
| addHttpParam('stale', opts, params); |
| addHttpParam('conflicts', opts, params); |
| addHttpParam('startkey', opts, params, true); |
| addHttpParam('start_key', opts, params, true); |
| addHttpParam('endkey', opts, params, true); |
| addHttpParam('end_key', opts, params, true); |
| addHttpParam('inclusive_end', opts, params); |
| addHttpParam('key', opts, params, true); |
| |
| // Format the list of parameters into a valid URI query string |
| params = params.join('&'); |
| params = params === '' ? '' : '?' + params; |
| |
| // If keys are supplied, issue a POST to circumvent GET query string limits |
| // see http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options |
| if (typeof opts.keys !== 'undefined') { |
| var MAX_URL_LENGTH = 2000; |
| // according to http://stackoverflow.com/a/417184/680742, |
| // the de facto URL length limit is 2000 characters |
| |
| var keysAsString = |
| 'keys=' + encodeURIComponent(JSON.stringify(opts.keys)); |
| if (keysAsString.length + params.length + 1 <= MAX_URL_LENGTH) { |
| // If the keys are short enough, do a GET. we do this to work around |
| // Safari not understanding 304s on POSTs (see pouchdb/pouchdb#1239) |
| params += (params[0] === '?' ? '&' : '?') + keysAsString; |
| } else { |
| method = 'POST'; |
| if (typeof fun === 'string') { |
| body = {keys: opts.keys}; |
| } else { // fun is {map : mapfun}, so append to this |
| fun.keys = opts.keys; |
| } |
| } |
| } |
| |
| // We are referencing a query defined in the design doc |
| if (typeof fun === 'string') { |
| var parts = parseViewName(fun); |
| return db.request({ |
| method: method, |
| url: '_design/' + parts[0] + '/_view/' + parts[1] + params, |
| body: body |
| }).then( |
| /* istanbul ignore next */ |
| function (result) { |
| // fail the entire request if the result contains an error |
| result.rows.forEach(function (row) { |
| if (row.value && row.value.error && row.value.error === "builtin_reduce_error") { |
| throw new Error(row.reason); |
| } |
| }); |
| |
| return result; |
| }) |
| .then(postprocessAttachments(opts)); |
| } |
| |
| // We are using a temporary view, terrible for performance, good for testing |
| body = body || {}; |
| Object.keys(fun).forEach(function (key) { |
| if (Array.isArray(fun[key])) { |
| body[key] = fun[key]; |
| } else { |
| body[key] = fun[key].toString(); |
| } |
| }); |
| return db.request({ |
| method: 'POST', |
| url: '_temp_view' + params, |
| body: body |
| }).then(postprocessAttachments(opts)); |
| } |
| |
| // custom adapters can define their own api._query |
| // and override the default behavior |
| /* istanbul ignore next */ |
| function customQuery(db, fun, opts) { |
| return new Promise(function (resolve, reject) { |
| db._query(fun, opts, function (err, res) { |
| if (err) { |
| return reject(err); |
| } |
| resolve(res); |
| }); |
| }); |
| } |
| |
| // custom adapters can define their own api._viewCleanup |
| // and override the default behavior |
| /* istanbul ignore next */ |
| function customViewCleanup(db) { |
| return new Promise(function (resolve, reject) { |
| db._viewCleanup(function (err, res) { |
| if (err) { |
| return reject(err); |
| } |
| resolve(res); |
| }); |
| }); |
| } |
| |
| function defaultsTo(value) { |
| return function (reason) { |
| /* istanbul ignore else */ |
| if (reason.status === 404) { |
| return value; |
| } else { |
| throw reason; |
| } |
| }; |
| } |
| |
| // returns a promise for a list of docs to update, based on the input docId. |
| // the order doesn't matter, because post-3.2.0, bulkDocs |
| // is an atomic operation in all three adapters. |
| function getDocsToPersist(docId, view, docIdsToChangesAndEmits) { |
| var metaDocId = '_local/doc_' + docId; |
| var defaultMetaDoc = {_id: metaDocId, keys: []}; |
| var docData = docIdsToChangesAndEmits.get(docId); |
| var indexableKeysToKeyValues = docData[0]; |
| var changes = docData[1]; |
| |
| function getMetaDoc() { |
| if (isGenOne(changes)) { |
| // generation 1, so we can safely assume initial state |
| // for performance reasons (avoids unnecessary GETs) |
| return Promise.resolve(defaultMetaDoc); |
| } |
| return view.db.get(metaDocId).catch(defaultsTo(defaultMetaDoc)); |
| } |
| |
| function getKeyValueDocs(metaDoc) { |
| if (!metaDoc.keys.length) { |
| // no keys, no need for a lookup |
| return Promise.resolve({rows: []}); |
| } |
| return view.db.allDocs({ |
| keys: metaDoc.keys, |
| include_docs: true |
| }); |
| } |
| |
| function processKeyValueDocs(metaDoc, kvDocsRes) { |
| var kvDocs = []; |
| var oldKeys = new Set(); |
| |
| for (var i = 0, len = kvDocsRes.rows.length; i < len; i++) { |
| var row = kvDocsRes.rows[i]; |
| var doc = row.doc; |
| if (!doc) { // deleted |
| continue; |
| } |
| kvDocs.push(doc); |
| oldKeys.add(doc._id); |
| doc._deleted = !indexableKeysToKeyValues.has(doc._id); |
| if (!doc._deleted) { |
| var keyValue = indexableKeysToKeyValues.get(doc._id); |
| if ('value' in keyValue) { |
| doc.value = keyValue.value; |
| } |
| } |
| } |
| var newKeys = mapToKeysArray(indexableKeysToKeyValues); |
| newKeys.forEach(function (key) { |
| if (!oldKeys.has(key)) { |
| // new doc |
| var kvDoc = { |
| _id: key |
| }; |
| var keyValue = indexableKeysToKeyValues.get(key); |
| if ('value' in keyValue) { |
| kvDoc.value = keyValue.value; |
| } |
| kvDocs.push(kvDoc); |
| } |
| }); |
| metaDoc.keys = uniq(newKeys.concat(metaDoc.keys)); |
| kvDocs.push(metaDoc); |
| |
| return kvDocs; |
| } |
| |
| return getMetaDoc().then(function (metaDoc) { |
| return getKeyValueDocs(metaDoc).then(function (kvDocsRes) { |
| return processKeyValueDocs(metaDoc, kvDocsRes); |
| }); |
| }); |
| } |
| |
| // updates all emitted key/value docs and metaDocs in the mrview database |
| // for the given batch of documents from the source database |
| function saveKeyValues(view, docIdsToChangesAndEmits, seq) { |
| var seqDocId = '_local/lastSeq'; |
| return view.db.get(seqDocId) |
| .catch(defaultsTo({_id: seqDocId, seq: 0})) |
| .then(function (lastSeqDoc) { |
| var docIds = mapToKeysArray(docIdsToChangesAndEmits); |
| return Promise.all(docIds.map(function (docId) { |
| return getDocsToPersist(docId, view, docIdsToChangesAndEmits); |
| })).then(function (listOfDocsToPersist) { |
| var docsToPersist = flatten(listOfDocsToPersist); |
| lastSeqDoc.seq = seq; |
| docsToPersist.push(lastSeqDoc); |
| // write all docs in a single operation, update the seq once |
| return view.db.bulkDocs({docs : docsToPersist}); |
| }); |
| }); |
| } |
| |
| function getQueue(view) { |
| var viewName = typeof view === 'string' ? view : view.name; |
| var queue = persistentQueues[viewName]; |
| if (!queue) { |
| queue = persistentQueues[viewName] = new TaskQueue(); |
| } |
| return queue; |
| } |
| |
| function updateView(view) { |
| return sequentialize(getQueue(view), function () { |
| return updateViewInQueue(view); |
| })(); |
| } |
| |
| function updateViewInQueue(view) { |
| // bind the emit function once |
| var mapResults; |
| var doc; |
| |
| function emit(key, value) { |
| var output = {id: doc._id, key: normalizeKey(key)}; |
| // Don't explicitly store the value unless it's defined and non-null. |
| // This saves on storage space, because often people don't use it. |
| if (typeof value !== 'undefined' && value !== null) { |
| output.value = normalizeKey(value); |
| } |
| mapResults.push(output); |
| } |
| |
| var mapFun = mapper(view.mapFun, emit); |
| |
| var currentSeq = view.seq || 0; |
| |
| function processChange(docIdsToChangesAndEmits, seq) { |
| return function () { |
| return saveKeyValues(view, docIdsToChangesAndEmits, seq); |
| }; |
| } |
| |
| var queue = new TaskQueue(); |
| |
| function processNextBatch() { |
| return view.sourceDB.changes({ |
| conflicts: true, |
| include_docs: true, |
| style: 'all_docs', |
| since: currentSeq, |
| limit: CHANGES_BATCH_SIZE |
| }).then(processBatch); |
| } |
| |
| function processBatch(response) { |
| var results = response.results; |
| if (!results.length) { |
| return; |
| } |
| var docIdsToChangesAndEmits = createDocIdsToChangesAndEmits(results); |
| queue.add(processChange(docIdsToChangesAndEmits, currentSeq)); |
| if (results.length < CHANGES_BATCH_SIZE) { |
| return; |
| } |
| return processNextBatch(); |
| } |
| |
| function createDocIdsToChangesAndEmits(results) { |
| var docIdsToChangesAndEmits = new Map(); |
| for (var i = 0, len = results.length; i < len; i++) { |
| var change = results[i]; |
| if (change.doc._id[0] !== '_') { |
| mapResults = []; |
| doc = change.doc; |
| |
| if (!doc._deleted) { |
| tryMap(view.sourceDB, mapFun, doc); |
| } |
| mapResults.sort(sortByKeyThenValue); |
| |
| var indexableKeysToKeyValues = createIndexableKeysToKeyValues(mapResults); |
| docIdsToChangesAndEmits.set(change.doc._id, [ |
| indexableKeysToKeyValues, |
| change.changes |
| ]); |
| } |
| currentSeq = change.seq; |
| } |
| return docIdsToChangesAndEmits; |
| } |
| |
| function createIndexableKeysToKeyValues(mapResults) { |
| var indexableKeysToKeyValues = new Map(); |
| var lastKey; |
| for (var i = 0, len = mapResults.length; i < len; i++) { |
| var emittedKeyValue = mapResults[i]; |
| var complexKey = [emittedKeyValue.key, emittedKeyValue.id]; |
| if (i > 0 && collate(emittedKeyValue.key, lastKey) === 0) { |
| complexKey.push(i); // dup key+id, so make it unique |
| } |
| indexableKeysToKeyValues.set(toIndexableString(complexKey), emittedKeyValue); |
| lastKey = emittedKeyValue.key; |
| } |
| return indexableKeysToKeyValues; |
| } |
| |
| return processNextBatch().then(function () { |
| return queue.finish(); |
| }).then(function () { |
| view.seq = currentSeq; |
| }); |
| } |
| |
| function reduceView(view, results, options) { |
| if (options.group_level === 0) { |
| delete options.group_level; |
| } |
| |
| var shouldGroup = options.group || options.group_level; |
| |
| var reduceFun = reducer(view.reduceFun); |
| |
| var groups = []; |
| var lvl = isNaN(options.group_level) ? Number.POSITIVE_INFINITY : |
| options.group_level; |
| results.forEach(function (e) { |
| var last = groups[groups.length - 1]; |
| var groupKey = shouldGroup ? e.key : null; |
| |
| // only set group_level for array keys |
| if (shouldGroup && Array.isArray(groupKey)) { |
| groupKey = groupKey.slice(0, lvl); |
| } |
| |
| if (last && collate(last.groupKey, groupKey) === 0) { |
| last.keys.push([e.key, e.id]); |
| last.values.push(e.value); |
| return; |
| } |
| groups.push({ |
| keys: [[e.key, e.id]], |
| values: [e.value], |
| groupKey: groupKey |
| }); |
| }); |
| results = []; |
| for (var i = 0, len = groups.length; i < len; i++) { |
| var e = groups[i]; |
| var reduceTry = tryReduce(view.sourceDB, reduceFun, e.keys, e.values, false); |
| if (reduceTry.error && reduceTry.error instanceof BuiltInError) { |
| // CouchDB returns an error if a built-in errors out |
| throw reduceTry.error; |
| } |
| results.push({ |
| // CouchDB just sets the value to null if a non-built-in errors out |
| value: reduceTry.error ? null : reduceTry.output, |
| key: e.groupKey |
| }); |
| } |
| // no total_rows/offset when reducing |
| return {rows: sliceResults(results, options.limit, options.skip)}; |
| } |
| |
| function queryView(view, opts) { |
| return sequentialize(getQueue(view), function () { |
| return queryViewInQueue(view, opts); |
| })(); |
| } |
| |
| function queryViewInQueue(view, opts) { |
| var totalRows; |
| var shouldReduce = view.reduceFun && opts.reduce !== false; |
| var skip = opts.skip || 0; |
| if (typeof opts.keys !== 'undefined' && !opts.keys.length) { |
| // equivalent query |
| opts.limit = 0; |
| delete opts.keys; |
| } |
| |
| function fetchFromView(viewOpts) { |
| viewOpts.include_docs = true; |
| return view.db.allDocs(viewOpts).then(function (res) { |
| totalRows = res.total_rows; |
| return res.rows.map(function (result) { |
| |
| // implicit migration - in older versions of PouchDB, |
| // we explicitly stored the doc as {id: ..., key: ..., value: ...} |
| // this is tested in a migration test |
| /* istanbul ignore next */ |
| if ('value' in result.doc && typeof result.doc.value === 'object' && |
| result.doc.value !== null) { |
| var keys = Object.keys(result.doc.value).sort(); |
| // this detection method is not perfect, but it's unlikely the user |
| // emitted a value which was an object with these 3 exact keys |
| var expectedKeys = ['id', 'key', 'value']; |
| if (!(keys < expectedKeys || keys > expectedKeys)) { |
| return result.doc.value; |
| } |
| } |
| |
| var parsedKeyAndDocId = parseIndexableString(result.doc._id); |
| return { |
| key: parsedKeyAndDocId[0], |
| id: parsedKeyAndDocId[1], |
| value: ('value' in result.doc ? result.doc.value : null) |
| }; |
| }); |
| }); |
| } |
| |
| function onMapResultsReady(rows) { |
| var finalResults; |
| if (shouldReduce) { |
| finalResults = reduceView(view, rows, opts); |
| } else { |
| finalResults = { |
| total_rows: totalRows, |
| offset: skip, |
| rows: rows |
| }; |
| } |
| if (opts.include_docs) { |
| var docIds = uniq(rows.map(rowToDocId)); |
| |
| return view.sourceDB.allDocs({ |
| keys: docIds, |
| include_docs: true, |
| conflicts: opts.conflicts, |
| attachments: opts.attachments, |
| binary: opts.binary |
| }).then(function (allDocsRes) { |
| var docIdsToDocs = new Map(); |
| allDocsRes.rows.forEach(function (row) { |
| docIdsToDocs.set(row.id, row.doc); |
| }); |
| rows.forEach(function (row) { |
| var docId = rowToDocId(row); |
| var doc = docIdsToDocs.get(docId); |
| if (doc) { |
| row.doc = doc; |
| } |
| }); |
| return finalResults; |
| }); |
| } else { |
| return finalResults; |
| } |
| } |
| |
| if (typeof opts.keys !== 'undefined') { |
| var keys = opts.keys; |
| var fetchPromises = keys.map(function (key) { |
| var viewOpts = { |
| startkey : toIndexableString([key]), |
| endkey : toIndexableString([key, {}]) |
| }; |
| return fetchFromView(viewOpts); |
| }); |
| return Promise.all(fetchPromises).then(flatten).then(onMapResultsReady); |
| } else { // normal query, no 'keys' |
| var viewOpts = { |
| descending : opts.descending |
| }; |
| var startkey; |
| var endkey; |
| if ('start_key' in opts) { |
| startkey = opts.start_key; |
| } |
| if ('startkey' in opts) { |
| startkey = opts.startkey; |
| } |
| if ('end_key' in opts) { |
| endkey = opts.end_key; |
| } |
| if ('endkey' in opts) { |
| endkey = opts.endkey; |
| } |
| if (typeof startkey !== 'undefined') { |
| viewOpts.startkey = opts.descending ? |
| toIndexableString([startkey, {}]) : |
| toIndexableString([startkey]); |
| } |
| if (typeof endkey !== 'undefined') { |
| var inclusiveEnd = opts.inclusive_end !== false; |
| if (opts.descending) { |
| inclusiveEnd = !inclusiveEnd; |
| } |
| |
| viewOpts.endkey = toIndexableString( |
| inclusiveEnd ? [endkey, {}] : [endkey]); |
| } |
| if (typeof opts.key !== 'undefined') { |
| var keyStart = toIndexableString([opts.key]); |
| var keyEnd = toIndexableString([opts.key, {}]); |
| if (viewOpts.descending) { |
| viewOpts.endkey = keyStart; |
| viewOpts.startkey = keyEnd; |
| } else { |
| viewOpts.startkey = keyStart; |
| viewOpts.endkey = keyEnd; |
| } |
| } |
| if (!shouldReduce) { |
| if (typeof opts.limit === 'number') { |
| viewOpts.limit = opts.limit; |
| } |
| viewOpts.skip = skip; |
| } |
| return fetchFromView(viewOpts).then(onMapResultsReady); |
| } |
| } |
| |
| function httpViewCleanup(db) { |
| return db.request({ |
| method: 'POST', |
| url: '_view_cleanup' |
| }); |
| } |
| |
| function localViewCleanup(db) { |
| return db.get('_local/' + localDocName).then(function (metaDoc) { |
| var docsToViews = new Map(); |
| Object.keys(metaDoc.views).forEach(function (fullViewName) { |
| var parts = parseViewName(fullViewName); |
| var designDocName = '_design/' + parts[0]; |
| var viewName = parts[1]; |
| var views = docsToViews.get(designDocName); |
| if (!views) { |
| views = new Set(); |
| docsToViews.set(designDocName, views); |
| } |
| views.add(viewName); |
| }); |
| var opts = { |
| keys : mapToKeysArray(docsToViews), |
| include_docs : true |
| }; |
| return db.allDocs(opts).then(function (res) { |
| var viewsToStatus = {}; |
| res.rows.forEach(function (row) { |
| var ddocName = row.key.substring(8); // cuts off '_design/' |
| docsToViews.get(row.key).forEach(function (viewName) { |
| var fullViewName = ddocName + '/' + viewName; |
| /* istanbul ignore if */ |
| if (!metaDoc.views[fullViewName]) { |
| // new format, without slashes, to support PouchDB 2.2.0 |
| // migration test in pouchdb's browser.migration.js verifies this |
| fullViewName = viewName; |
| } |
| var viewDBNames = Object.keys(metaDoc.views[fullViewName]); |
| // design doc deleted, or view function nonexistent |
| var statusIsGood = row.doc && row.doc.views && |
| row.doc.views[viewName]; |
| viewDBNames.forEach(function (viewDBName) { |
| viewsToStatus[viewDBName] = |
| viewsToStatus[viewDBName] || statusIsGood; |
| }); |
| }); |
| }); |
| var dbsToDelete = Object.keys(viewsToStatus).filter( |
| function (viewDBName) { return !viewsToStatus[viewDBName]; }); |
| var destroyPromises = dbsToDelete.map(function (viewDBName) { |
| return sequentialize(getQueue(viewDBName), function () { |
| return new db.constructor(viewDBName, db.__opts).destroy(); |
| })(); |
| }); |
| return Promise.all(destroyPromises).then(function () { |
| return {ok: true}; |
| }); |
| }); |
| }, defaultsTo({ok: true})); |
| } |
| |
| function queryPromised(db, fun, opts) { |
| /* istanbul ignore next */ |
| if (typeof db._query === 'function') { |
| return customQuery(db, fun, opts); |
| } |
| if (isRemote(db)) { |
| return httpQuery(db, fun, opts); |
| } |
| |
| if (typeof fun !== 'string') { |
| // temp_view |
| checkQueryParseError(opts, fun); |
| |
| tempViewQueue.add(function () { |
| var createViewPromise = createView( |
| /* sourceDB */ db, |
| /* viewName */ 'temp_view/temp_view', |
| /* mapFun */ fun.map, |
| /* reduceFun */ fun.reduce, |
| /* temporary */ true, |
| /* localDocName */ localDocName); |
| return createViewPromise.then(function (view) { |
| return fin(updateView(view).then(function () { |
| return queryView(view, opts); |
| }), function () { |
| return view.db.destroy(); |
| }); |
| }); |
| }); |
| return tempViewQueue.finish(); |
| } else { |
| // persistent view |
| var fullViewName = fun; |
| var parts = parseViewName(fullViewName); |
| var designDocName = parts[0]; |
| var viewName = parts[1]; |
| return db.get('_design/' + designDocName).then(function (doc) { |
| var fun = doc.views && doc.views[viewName]; |
| |
| if (!fun) { |
| // basic validator; it's assumed that every subclass would want this |
| throw new NotFoundError('ddoc ' + doc._id + ' has no view named ' + |
| viewName); |
| } |
| |
| ddocValidator(doc, viewName); |
| checkQueryParseError(opts, fun); |
| |
| var createViewPromise = createView( |
| /* sourceDB */ db, |
| /* viewName */ fullViewName, |
| /* mapFun */ fun.map, |
| /* reduceFun */ fun.reduce, |
| /* temporary */ false, |
| /* localDocName */ localDocName); |
| return createViewPromise.then(function (view) { |
| if (opts.stale === 'ok' || opts.stale === 'update_after') { |
| if (opts.stale === 'update_after') { |
| nextTick(function () { |
| updateView(view); |
| }); |
| } |
| return queryView(view, opts); |
| } else { // stale not ok |
| return updateView(view).then(function () { |
| return queryView(view, opts); |
| }); |
| } |
| }); |
| }); |
| } |
| } |
| |
| function abstractQuery(fun, opts, callback) { |
| var db = this; |
| if (typeof opts === 'function') { |
| callback = opts; |
| opts = {}; |
| } |
| opts = opts ? coerceOptions(opts) : {}; |
| |
| if (typeof fun === 'function') { |
| fun = {map : fun}; |
| } |
| |
| var promise = Promise.resolve().then(function () { |
| return queryPromised(db, fun, opts); |
| }); |
| promisedCallback(promise, callback); |
| return promise; |
| } |
| |
| var abstractViewCleanup = callbackify(function () { |
| var db = this; |
| /* istanbul ignore next */ |
| if (typeof db._viewCleanup === 'function') { |
| return customViewCleanup(db); |
| } |
| if (isRemote(db)) { |
| return httpViewCleanup(db); |
| } |
| return localViewCleanup(db); |
| }); |
| |
| return { |
| query: abstractQuery, |
| viewCleanup: abstractViewCleanup |
| }; |
| } |
| |
| export default createAbstractMapReduce; |