UNPKG

pouchdb-find

Version:
1,715 lines (1,492 loc) 151 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.pouchdbFind = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ 'use strict'; var upsert = _dereq_(4); var utils = _dereq_(5); var Promise = utils.Promise; function stringify(input) { if (!input) { return 'undefined'; // backwards compat for empty reduce } // for backwards compat with mapreduce, functions/strings are stringified // as-is. everything else is JSON-stringified. switch (typeof input) { case 'function': // e.g. a mapreduce map return input.toString(); case 'string': // e.g. a mapreduce built-in _reduce function return input.toString(); default: // e.g. a JSON object in the case of mango queries return JSON.stringify(input); } } module.exports = function (opts) { var sourceDB = opts.db; var viewName = opts.viewName; var mapFun = opts.map; var reduceFun = opts.reduce; var temporary = opts.temporary; var pluginName = opts.pluginName; // the "undefined" part is for backwards compatibility var viewSignature = stringify(mapFun) + stringify(reduceFun) + 'undefined'; if (!temporary && sourceDB._cachedViews) { var cachedView = sourceDB._cachedViews[viewSignature]; if (cachedView) { return Promise.resolve(cachedView); } } return sourceDB.info().then(function (info) { var depDbName = info.db_name + '-mrview-' + (temporary ? 'temp' : utils.MD5(viewSignature)); // save the view name in the source PouchDB so it can be cleaned up if necessary // (e.g. when the _design doc is deleted, remove all associated view data) function diffFunction(doc) { doc.views = doc.views || {}; var fullViewName = viewName; if (fullViewName.indexOf('/') === -1) { fullViewName = viewName + '/' + viewName; } var depDbs = doc.views[fullViewName] = doc.views[fullViewName] || {}; /* istanbul ignore if */ if (depDbs[depDbName]) { return; // no update necessary } depDbs[depDbName] = true; return doc; } return upsert(sourceDB, '_local/' + pluginName, diffFunction).then(function () { return sourceDB.registerDependentDatabase(depDbName).then(function (res) { var db = res.db; db.auto_compaction = true; var view = { name: depDbName, db: db, sourceDB: sourceDB, adapter: sourceDB.adapter, mapFun: mapFun, reduceFun: reduceFun }; return view.db.get('_local/lastSeq')["catch"](function (err) { /* istanbul ignore if */ if (err.status !== 404) { throw err; } }).then(function (lastSeqDoc) { view.seq = lastSeqDoc ? lastSeqDoc.seq : 0; if (!temporary) { sourceDB._cachedViews = sourceDB._cachedViews || {}; sourceDB._cachedViews[viewSignature] = view; view.db.on('destroyed', function () { delete sourceDB._cachedViews[viewSignature]; }); } return view; }); }); }); }); }; },{"4":4,"5":5}],2:[function(_dereq_,module,exports){ (function (process){ 'use strict'; var pouchCollate = _dereq_(38); var TaskQueue = _dereq_(3); var collate = pouchCollate.collate; var toIndexableString = pouchCollate.toIndexableString; var normalizeKey = pouchCollate.normalizeKey; var createView = _dereq_(1); var log; /* istanbul ignore else */ if ((typeof console !== 'undefined') && (typeof console.log === 'function')) { log = Function.prototype.bind.call(console.log, console); } else { log = function () {}; } var utils = _dereq_(5); var Promise = utils.Promise; var persistentQueues = {}; var tempViewQueue = new TaskQueue(); var CHANGES_BATCH_SIZE = 50; function QueryParseError(message) { this.status = 400; this.name = 'query_parse_error'; this.message = message; this.error = true; try { Error.captureStackTrace(this, QueryParseError); } catch (e) {} } utils.inherits(QueryParseError, Error); function NotFoundError(message) { this.status = 404; this.name = 'not_found'; this.message = message; this.error = true; try { Error.captureStackTrace(this, NotFoundError); } catch (e) {} } utils.inherits(NotFoundError, Error); 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 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 emitError(db, e) { try { db.emit('error', e); } catch (err) { console.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.'); console.error(e); } } function tryCode(db, fun, args) { // emit an event if there was an error thrown by a map/reduce function. // putting try/catches in a single function also avoids deoptimizations. try { return { output : fun.apply(null, args) }; } catch (e) { emitError(db, e); return {error: e}; } } 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}'); } } if (options.group_level) { if (typeof options.group_level !== 'number') { throw new QueryParseError('Invalid value for integer: "' + options.group_level + '"'); } if (options.group_level < 0) { throw new QueryParseError('Invalid value for positive integer: ' + '"' + options.group_level + '"'); } } } function defaultsTo(value) { return function (reason) { /* istanbul ignore else */ if (reason.status === 404) { return value; } else { throw reason; } }; } function createIndexer(def) { var pluginName = def.name; var mapper = def.mapper; var reducer = def.reducer; var ddocValidator = def.ddocValidator; // 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[docId]; var indexableKeysToKeyValues = docData.indexableKeysToKeyValues; var changes = docData.changes; 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 processKvDocs(metaDoc, kvDocsRes) { var kvDocs = []; var oldKeysMap = {}; 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); oldKeysMap[doc._id] = true; doc._deleted = !indexableKeysToKeyValues[doc._id]; if (!doc._deleted) { var keyValue = indexableKeysToKeyValues[doc._id]; if ('value' in keyValue) { doc.value = keyValue.value; } } } var newKeys = Object.keys(indexableKeysToKeyValues); newKeys.forEach(function (key) { if (!oldKeysMap[key]) { // new doc var kvDoc = { _id: key }; var keyValue = indexableKeysToKeyValues[key]; if ('value' in keyValue) { kvDoc.value = keyValue.value; } kvDocs.push(kvDoc); } }); metaDoc.keys = utils.uniq(newKeys.concat(metaDoc.keys)); kvDocs.push(metaDoc); return kvDocs; } return getMetaDoc().then(function (metaDoc) { return getKeyValueDocs(metaDoc).then(function (kvDocsRes) { return processKvDocs(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 = Object.keys(docIdsToChangesAndEmits); return Promise.all(docIds.map(function (docId) { return getDocsToPersist(docId, view, docIdsToChangesAndEmits); })).then(function (listOfDocsToPersist) { var docsToPersist = utils.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 utils.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(); return new Promise(function (resolve, reject) { function complete() { queue.finish().then(function () { view.seq = currentSeq; resolve(); }); } function processNextBatch() { view.sourceDB.changes({ conflicts: true, include_docs: true, style: 'all_docs', since: currentSeq, limit: CHANGES_BATCH_SIZE }).on('complete', function (response) { var results = response.results; if (!results.length) { return complete(); } var docIdsToChangesAndEmits = {}; for (var i = 0, l = results.length; i < l; i++) { var change = results[i]; if (change.doc._id[0] !== '_') { mapResults = []; doc = change.doc; if (!doc._deleted) { tryCode(view.sourceDB, mapFun, [doc]); } mapResults.sort(sortByKeyThenValue); var indexableKeysToKeyValues = {}; var lastKey; for (var j = 0, jl = mapResults.length; j < jl; j++) { var obj = mapResults[j]; var complexKey = [obj.key, obj.id]; if (collate(obj.key, lastKey) === 0) { complexKey.push(j); // dup key+id, so make it unique } var indexableKey = toIndexableString(complexKey); indexableKeysToKeyValues[indexableKey] = obj; lastKey = obj.key; } docIdsToChangesAndEmits[change.doc._id] = { indexableKeysToKeyValues: indexableKeysToKeyValues, changes: change.changes }; } currentSeq = change.seq; } queue.add(processChange(docIdsToChangesAndEmits, currentSeq)); if (results.length < CHANGES_BATCH_SIZE) { return complete(); } return processNextBatch(); }).on('error', onError); /* istanbul ignore next */ function onError(err) { reject(err); } } processNextBatch(); }); } 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 = options.group_level; results.forEach(function (e) { var last = groups[groups.length - 1]; var key = shouldGroup ? e.key : null; // only set group_level for array keys if (shouldGroup && Array.isArray(key) && typeof lvl === 'number') { key = key.length > lvl ? key.slice(0, lvl) : key; } if (last && collate(last.key[0][0], key) === 0) { last.key.push([key, e.id]); last.value.push(e.value); return; } groups.push({key: [ [key, e.id] ], value: [e.value]}); }); for (var i = 0, len = groups.length; i < len; i++) { var e = groups[i]; var reduceTry = tryCode(view.sourceDB, reduceFun, [e.key, e.value, false]); // TODO: can't do instanceof BuiltInError because this class is buried // in mapreduce.js if (reduceTry.error && /BuiltInError/.test(reduceTry.error.constructor)) { // CouchDB returns an error if a built-in errors out throw reduceTry.error; } // CouchDB just sets the value to null if a non-built-in errors out e.value = reduceTry.error ? null : reduceTry.output; e.key = e.key[0][0]; } // no total_rows/offset when reducing return {rows: sliceResults(groups, options.limit, options.skip)}; } function queryView(view, opts) { return utils.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 = pouchCollate.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 = utils.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 = {}; allDocsRes.rows.forEach(function (row) { if (row.doc) { docIdsToDocs['$' + row.id] = row.doc; } }); rows.forEach(function (row) { var docId = rowToDocId(row); var doc = docIdsToDocs['$' + docId]; if (doc) { row.doc = doc; } }); return finalResults; }); } else { return finalResults; } } var flatten = function (array) { return array.reduce(function (prev, cur) { return prev.concat(cur); }); }; 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 }; if (typeof opts.startkey !== 'undefined') { viewOpts.startkey = opts.descending ? toIndexableString([opts.startkey, {}]) : toIndexableString([opts.startkey]); } if (typeof opts.endkey !== 'undefined') { var inclusiveEnd = opts.inclusive_end !== false; if (opts.descending) { inclusiveEnd = !inclusiveEnd; } viewOpts.endkey = toIndexableString(inclusiveEnd ? [opts.endkey, {}] : [opts.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 localViewCleanup(db) { return db.get('_local/' + pluginName).then(function (metaDoc) { var docsToViews = {}; Object.keys(metaDoc.views).forEach(function (fullViewName) { var parts = parseViewName(fullViewName); var designDocName = '_design/' + parts[0]; var viewName = parts[1]; docsToViews[designDocName] = docsToViews[designDocName] || {}; docsToViews[designDocName][viewName] = true; }); var opts = { keys : Object.keys(docsToViews), include_docs : true }; return db.allDocs(opts).then(function (res) { var viewsToStatus = {}; res.rows.forEach(function (row) { var ddocName = row.key.substring(8); Object.keys(docsToViews[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 utils.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) { if (typeof fun !== 'string') { // temp_view checkQueryParseError(opts, fun); var createViewOpts = { db : db, viewName : 'temp_view/temp_view', map : fun.map, reduce : fun.reduce, temporary : true, pluginName: pluginName }; tempViewQueue.add(function () { return createView(createViewOpts).then(function (view) { function cleanup() { return view.db.destroy(); } return utils.fin(updateView(view).then(function () { return queryView(view, opts); }), cleanup); }); }); 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 createViewOpts = { db : db, viewName : fullViewName, map : fun.map, reduce : fun.reduce, pluginName: pluginName }; return createView(createViewOpts).then(function (view) { if (opts.stale === 'ok' || opts.stale === 'update_after') { if (opts.stale === 'update_after') { process.nextTick(function () { updateView(view); }); } return queryView(view, opts); } else { // stale not ok return updateView(view).then(function () { return queryView(view, opts); }); } }); }); } } var query = function (fun, opts, callback) { var db = this; if (typeof opts === 'function') { callback = opts; opts = {}; } opts = utils.extend(true, {}, opts); if (typeof fun === 'function') { fun = {map : fun}; } var promise = Promise.resolve().then(function () { return queryPromised(db, fun, opts); }); utils.promisedCallback(promise, callback); return promise; }; var viewCleanup = utils.callbackify(function () { var db = this; return localViewCleanup(db); }); return { query: query, viewCleanup: viewCleanup }; } module.exports = createIndexer; }).call(this,_dereq_(45)) },{"1":1,"3":3,"38":38,"45":45,"5":5}],3:[function(_dereq_,module,exports){ 'use strict'; /* * Simple task queue to sequentialize actions. Assumes callbacks will eventually fire (once). */ var Promise = _dereq_(5).Promise; function TaskQueue() { this.promise = new Promise(function (fulfill) {fulfill(); }); } TaskQueue.prototype.add = function (promiseFactory) { this.promise = this.promise["catch"](function () { // just recover }).then(function () { return promiseFactory(); }); return this.promise; }; TaskQueue.prototype.finish = function () { return this.promise; }; module.exports = TaskQueue; },{"5":5}],4:[function(_dereq_,module,exports){ 'use strict'; var upsert = _dereq_(41).upsert; module.exports = function (db, doc, diffFun) { return upsert.apply(db, [doc, diffFun]); }; },{"41":41}],5:[function(_dereq_,module,exports){ (function (process){ 'use strict'; /* istanbul ignore if */ exports.Promise = _dereq_(42); exports.inherits = _dereq_(23); exports.extend = _dereq_(40); var argsarray = _dereq_(18); /* istanbul ignore next */ exports.promisedCallback = function (promise, callback) { if (callback) { promise.then(function (res) { process.nextTick(function () { callback(null, res); }); }, function (reason) { process.nextTick(function () { callback(reason); }); }); } return promise; }; /* istanbul ignore next */ exports.callbackify = function (fun) { return argsarray(function (args) { var cb = args.pop(); var promise = fun.apply(this, args); if (typeof cb === 'function') { exports.promisedCallback(promise, cb); } return promise; }); }; // Promise finally util similar to Q.finally /* istanbul ignore next */ exports.fin = function (promise, cb) { return promise.then(function (res) { var promise2 = cb(); if (typeof promise2.then === 'function') { return promise2.then(function () { return res; }); } return res; }, function (reason) { var promise2 = cb(); if (typeof promise2.then === 'function') { return promise2.then(function () { throw reason; }); } throw reason; }); }; exports.sequentialize = function (queue, promiseFactory) { return function () { var args = arguments; var that = this; return queue.add(function () { return promiseFactory.apply(that, args); }); }; }; exports.flatten = function (arrs) { var res = []; for (var i = 0, len = arrs.length; i < len; i++) { res = res.concat(arrs[i]); } return res; }; // uniq an array of strings, order not guaranteed // similar to underscore/lodash _.uniq exports.uniq = function (arr) { var map = {}; for (var i = 0, len = arr.length; i < len; i++) { map['$' + arr[i]] = true; } var keys = Object.keys(map); var output = new Array(keys.length); for (i = 0, len = keys.length; i < len; i++) { output[i] = keys[i].substring(1); } return output; }; var crypto = _dereq_(19); var Md5 = _dereq_(46); exports.MD5 = function (string) { /* istanbul ignore else */ if (!process.browser) { return crypto.createHash('md5').update(string).digest('hex'); } else { return Md5.hash(string); } }; }).call(this,_dereq_(45)) },{"18":18,"19":19,"23":23,"40":40,"42":42,"45":45,"46":46}],6:[function(_dereq_,module,exports){ 'use strict'; var massageCreateIndexRequest = _dereq_(16); function createIndex(db, requestDef, callback) { requestDef = massageCreateIndexRequest(requestDef); db.request({ method: 'POST', url: '_index', body: requestDef }, callback); } function find(db, requestDef, callback) { db.request({ method: 'POST', url: '_find', body: requestDef }, callback); } function getIndexes(db, callback) { db.request({ method: 'GET', url: '_index' }, callback); } function deleteIndex(db, indexDef, callback) { var ddoc = indexDef.ddoc; var type = indexDef.type || 'json'; var name = indexDef.name; if (!ddoc) { return callback(new Error('you must provide an index\'s ddoc')); } if (!name) { return callback(new Error('you must provide an index\'s name')); } var url = '_index/' + [ddoc, type, name].map(encodeURIComponent).join('/'); db.request({ method: 'DELETE', url: url }, callback); } exports.createIndex = createIndex; exports.find = find; exports.getIndexes = getIndexes; exports.deleteIndex = deleteIndex; },{"16":16}],7:[function(_dereq_,module,exports){ 'use strict'; var localUtils = _dereq_(15); var abstractMapReduce = _dereq_(2); var parseField = localUtils.parseField; // // One thing about these mappers: // // Per the advice of John-David Dalton (http://youtu.be/NthmeLEhDDM), // what you want to do in this case is optimize for the smallest possible // function, since that's the thing that gets run over and over again. // // This code would be a lot simpler if all the if/elses were inside // the function, but it would also be a lot less performant. // function createDeepMultiMapper(fields, emit) { return function (doc) { var toEmit = []; for (var i = 0, iLen = fields.length; i < iLen; i++) { var parsedField = parseField(fields[i]); var value = doc; for (var j = 0, jLen = parsedField.length; j < jLen; j++) { var key = parsedField[j]; value = value[key]; if (!value) { break; } } toEmit.push(value); } emit(toEmit); }; } function createDeepSingleMapper(field, emit) { var parsedField = parseField(field); return function (doc) { var value = doc; for (var i = 0, len = parsedField.length; i < len; i++) { var key = parsedField[i]; value = value[key]; if (!value) { return; // do nothing } } emit(value); }; } function createShallowSingleMapper(field, emit) { return function (doc) { emit(doc[field]); }; } function createShallowMultiMapper(fields, emit) { return function (doc) { var toEmit = []; for (var i = 0, len = fields.length; i < len; i++) { toEmit.push(doc[fields[i]]); } emit(toEmit); }; } function checkShallow(fields) { for (var i = 0, len = fields.length; i < len; i++) { var field = fields[i]; if (field.indexOf('.') !== -1) { return false; } } return true; } function createMapper(fields, emit) { var isShallow = checkShallow(fields); var isSingle = fields.length === 1; // notice we try to optimize for the most common case, // i.e. single shallow indexes if (isShallow) { if (isSingle) { return createShallowSingleMapper(fields[0], emit); } else { // multi return createShallowMultiMapper(fields, emit); } } else { // deep if (isSingle) { return createDeepSingleMapper(fields[0], emit); } else { // multi return createDeepMultiMapper(fields, emit); } } } function mapper(mapFunDef, emit) { // mapFunDef is a list of fields var fields = Object.keys(mapFunDef.fields); return createMapper(fields, emit); } /* istanbul ignore next */ function reducer(/*reduceFunDef*/) { throw new Error('reduce not supported'); } function ddocValidator(ddoc, viewName) { var view = ddoc.views[viewName]; // This doesn't actually need to be here apparently, but // I feel safer keeping it. /* istanbul ignore if */ if (!view.map || !view.map.fields) { throw new Error('ddoc ' + ddoc._id +' with view ' + viewName + ' doesn\'t have map.fields defined. ' + 'maybe it wasn\'t created by this plugin?'); } } var abstractMapper = abstractMapReduce({ name: 'indexes', mapper: mapper, reducer: reducer, ddocValidator: ddocValidator }); module.exports = abstractMapper; },{"15":15,"2":2}],8:[function(_dereq_,module,exports){ 'use strict'; var utils = _dereq_(17); var log = utils.log; var pouchUpsert = _dereq_(41); var abstractMapper = _dereq_(7); var localUtils = _dereq_(15); var validateIndex = localUtils.validateIndex; var massageIndexDef = localUtils.massageIndexDef; var massageCreateIndexRequest = _dereq_(16); function upsert(db, docId, diffFun) { return pouchUpsert.upsert.call(db, docId, diffFun); } function createIndex(db, requestDef) { requestDef = massageCreateIndexRequest(requestDef); var originalIndexDef = utils.clone(requestDef.index); requestDef.index = massageIndexDef(requestDef.index); validateIndex(requestDef.index); var md5 = utils.MD5(JSON.stringify(requestDef)); var viewName = requestDef.name || ('idx-' + md5); var ddocName = requestDef.ddoc || ('idx-' + md5); var ddocId = '_design/' + ddocName; var hasInvalidLanguage = false; var viewExists = false; function updateDdoc(doc) { if (doc._rev && doc.language !== 'query') { hasInvalidLanguage = true; } doc.language = 'query'; doc.views = doc.views || {}; viewExists = !!doc.views[viewName]; doc.views[viewName] = { map: { fields: utils.mergeObjects(requestDef.index.fields) }, reduce: '_count', options: { def: originalIndexDef } }; return doc; } log('creating index', ddocId); return upsert(db, ddocId, updateDdoc).then(function () { if (hasInvalidLanguage) { throw new Error('invalid language for ddoc with id "' + ddocId + '" (should be "query")'); } }).then(function () { // kick off a build // TODO: abstract-pouchdb-mapreduce should support auto-updating // TODO: should also use update_after, but pouchdb/pouchdb#3415 blocks me var signature = ddocName + '/' + viewName; return abstractMapper.query.call(db, signature, { limit: 0, reduce: false }).then(function () { return { id: ddocId, name: viewName, result: viewExists ? 'exists' : 'created' }; }); }); } module.exports = createIndex; },{"15":15,"16":16,"17":17,"41":41,"7":7}],9:[function(_dereq_,module,exports){ 'use strict'; var abstractMapper = _dereq_(7); var upsert = _dereq_(4); function deleteIndex(db, index) { if (!index.ddoc) { throw new Error('you must supply an index.ddoc when deleting'); } if (!index.name) { throw new Error('you must supply an index.name when deleting'); } var docId = index.ddoc; var viewName = index.name; function deltaFun (doc) { if (Object.keys(doc.views).length === 1 && doc.views[viewName]) { // only one view in this ddoc, delete the whole ddoc return {_id: docId, _deleted: true}; } // more than one view here, just remove the view delete doc.views[viewName]; return doc; } return upsert(db, docId, deltaFun).then(function () { return abstractMapper.viewCleanup.apply(db); }).then(function () { return {ok: true}; }); } module.exports = deleteIndex; },{"4":4,"7":7}],10:[function(_dereq_,module,exports){ 'use strict'; // // Do an in-memory filtering of rows that aren't covered by the index. // E.g. if the user is asking for foo=1 and bar=2, but the index // only covers "foo", then this in-memory filter would take care of // "bar". // var collate = _dereq_(38).collate; var localUtils = _dereq_(15); var isCombinationalField = localUtils.isCombinationalField; var getKey = localUtils.getKey; var getValue = localUtils.getValue; var parseField = localUtils.parseField; var utils = _dereq_(17); // this would just be "return doc[field]", but fields // can be "deep" due to dot notation function getFieldFromDoc(doc, parsedField) { var value = doc; for (var i = 0, len = parsedField.length; i < len; i++) { var key = parsedField[i]; value = value[key]; if (!value) { break; } } return value; } function createCriterion(userOperator, userValue, parsedField) { // compare the value of the field in the doc // to the user-supplied value, using the couchdb collation scheme function getDocFieldCollate(doc) { return collate(getFieldFromDoc(doc, parsedField), userValue); } function fieldExists(doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); return typeof docFieldValue !== 'undefined' && docFieldValue !== null; } function fieldIsArray (doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); return fieldExists(doc) && docFieldValue instanceof Array; } function arrayContainsValue (doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); return userValue.some(function (val) { if (docFieldValue instanceof Array) { return docFieldValue.indexOf(val) > -1; } return docFieldValue === val; }); } function arrayContainsAllValues (doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); return userValue.every(function (val) { return docFieldValue.indexOf(val) > -1; }); } function arraySize (doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); return docFieldValue.length === userValue; } function modField (doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); var divisor = userValue[0]; var mod = userValue[1]; if (divisor === 0) { throw new Error('Bad divisor, cannot divide by zero'); } if (parseInt(divisor, 10) !== divisor ) { throw new Error('Divisor is not an integer'); } if (parseInt(mod, 10) !== mod ) { throw new Error('Modulus is not an integer'); } if (parseInt(docFieldValue, 10) !== docFieldValue) { return false; } return docFieldValue % divisor === mod; } function regexMatch(doc) { var re = new RegExp(userValue); var docFieldValue = getFieldFromDoc(doc, parsedField); return re.test(docFieldValue); } switch (userOperator) { case '$eq': return function (doc) { return fieldExists(doc) && getDocFieldCollate(doc) === 0; }; case '$lte': return function (doc) { return fieldExists(doc) && getDocFieldCollate(doc) <= 0; }; case '$gte': return function (doc) { return fieldExists(doc) && getDocFieldCollate(doc) >= 0; }; case '$lt': return function (doc) { return fieldExists(doc) && getDocFieldCollate(doc) < 0; }; case '$gt': return function (doc) { return fieldExists(doc) && getDocFieldCollate(doc) > 0; }; case '$exists': return function (doc) { return fieldExists(doc); }; case '$ne': return function (doc) { // might have to check multiple values, so I store this in an array var docFieldValue = getFieldFromDoc(doc, parsedField); return userValue.every(function (neValue) { return collate(docFieldValue, neValue) !== 0; }); }; case '$in': return function (doc) { return fieldExists(doc) && arrayContainsValue(doc); }; case '$nin': return function (doc) { return fieldExists(doc) && !arrayContainsValue(doc); }; case '$size': return function (doc) { return fieldIsArray(doc) && arraySize(doc); }; case '$all': return function (doc) { return fieldIsArray(doc) && arrayContainsAllValues(doc); }; case '$mod': return function (doc) { return fieldExists(doc) && modField(doc); }; case '$regex': return function (doc) { return fieldExists(doc) && regexMatch(doc); }; case '$elemMatch': return function (doc) { var docFieldValue = getFieldFromDoc(doc, parsedField); if (!fieldIsArray(doc)) { return false;} // Not the prettiest code I've ever written so I think I need to explain what I'm doing // I get the array field that we want to do the $elemMatch on and then call createCriterion // with a fake document just to check if this operator passes or not. If any of them do // then this document is a match return docFieldValue.some(function (value) { return Object.keys(userValue).every(function (matcher) { return createCriterion(matcher, userValue[matcher], 'a')({'a': value}); }); }); }; } throw new Error('unknown operator "' + parsedField[0] + '" - should be one of $eq, $lte, $lt, $gt, $gte, $exists, $ne, $in, ' + '$nin, $size, $mod or $all'); } function createCombinationalCriterion (operator, selectors) { var criterions = []; //The $not selector isn't an array, so convert it to an array selectors = (selectors instanceof Array) ? selectors : [selectors]; selectors.forEach(function (selector) { Object.keys(selector).forEach(function (field) { var matcher = selector[field]; var parsedField = parseField(field); Object.keys(matcher).forEach(function (userOperator) { var userValue = matcher[userOperator]; var out = createCriterion(userOperator, userValue, parsedField); criterions.push(out); }); }); }); if (operator === '$or') { return function (doc) { return criterions.some(function (criterion) { return criterion(doc); }); }; } if (operator === '$not') { return function (doc) { return !criterions[0](doc); }; } // '$nor' return function (doc) { return !criterions.find(function (criterion) { return criterion(doc); }); }; } function createFilterRowFunction(requestDef, inMemoryFields) { var criteria = []; inMemoryFields.forEach(function (field) { var matcher = requestDef.selector[field]; var parsedField = parseField(field); if (!matcher) { // no filtering necessary; this field is just needed for sorting return; } if (isCombinationalField(field)) { var criterion = createCombinationalCriterion(field, matcher); criteria.push(criterion); return; } Object.keys(matcher).forEach(function (userOperator) { var userValue = matcher[userOperator]; var criterion = createCriterion(userOperator, userValue, parsedField); criteria.push(criterion); }); }); return function filterRowFunction(row) { for (var i = 0, len = criteria.length; i < len; i++) { var criterion = criteria[i]; if (!criterion(row.doc)) { return false; } } return true; }; } // create a comparator based on the sort object function createFieldSorter(sort) { function getFieldValuesAsArray(doc) { return sort.map(function (sorting) { var fieldName = getKey(sorting); var parsedField = parseField(fieldName); var docFieldValue = getFieldFromDoc(doc, parsedField); return docFieldValue; }); } return function (aRow, bRow) { var aFieldValues = getFieldValuesAsArray(aRow.doc); var bFieldValues = getFieldValuesAsArray(bRow.doc); var collation = collate(aFieldValues, bFieldValues); if (collation !== 0) { return collation; } // this is what mango seems to do return utils.compare(aRow.doc._id, bRow.doc._id); }; } // filter any fields not covered by the index function filterInMemoryFields(rows, requestDef, inMemoryFields) { var filter = createFilterRowFunction(requestDef, inMemoryFields); rows = rows.filter(filter); if (requestDef.sort) { // in-memory sort var fieldSorter = createFieldSorter(requestDef.sort); rows = rows.sort(fieldSorter); if (typeof requestDef.sort[0] !== 'string' && getValue(requestDef.sort[0]) === 'desc') { rows = rows.reverse(); } } if ('limit' in requestDef || 'skip' in requestDef) { // have to do the limit in-memory var skip = requestDef.skip || 0; var limit = ('limit' in requestDef ? requestDef.limit : rows.length) + skip; rows = rows.slice(skip, limit); } return rows; } module.exports = filterInMemoryFields; },{"15":15,"17":17,"38":38}],11:[function(_dereq_,module,exports){ 'use strict'; var utils = _dereq_(17); var clone = utils.clone; var getIndexes = _dereq_(13); var collate = _dereq_(38).collate; var abstractMapper = _dereq_(7); var planQuery = _dereq_(12); var localUtils = _dereq_(15); var filterInMemoryFields = _dereq_(10); var massageSelector = localUtils.massageSelector; var massageSort = localUtils.massageSort; var getValue = localUtils.getValue; var validateFindRequest = localUtils.validateFindRequest; var reverseOptions = localUtils.reverseOptions; var filterInclusiveStart = localUtils.filterInclusiveStart; var Promise = utils.Promise; function indexToSignature(index) { // remove '_design/' return index.ddoc.substring(8) + '/' + index.name; } function doAllDocs(db, originalOpts) { var opts = clone(originalOpts); // CouchDB responds in weird ways when you provide a non-string to _id; // we mimic the behavior for consistency. See issue66 tests for details. if (opts.descending) { if ('endkey' in opts && typeof opts.endkey !== 'string') { opts.endkey = ''; } if ('startkey' in opts && typeof opts.startkey !== 'string') { opts.limit = 0; } } else { if ('startkey' in opts && typeof opts.startkey !== 'string') { opts.startkey = ''; } if ('endkey' in opts && typeof opts.endkey !== 'string') { opts.limit = 0; } } if ('key' in opts && typeof opts.key !== 'string') { opts.limit = 0; } return db.allDocs(opts); } function find(db, requestDef) { if (requestDef.selector) { requestDef.selector = massageSelector(requestDef.selector); } if (requestDef.sort) { requestDef.sort = massageSort(requestDef.sort); } validateFindRequest(requestDef); return getIndexes(db).then(function (getIndexesRes) { var queryPlan = planQuery(requestDef, getIndexesRes.indexes); var indexToUse = queryPlan.index; var opts = utils.extend(true, { include_docs: true, reduce: false }, queryPlan.queryOpts); if ('startkey' in opts && 'endkey' in opts && collate(opts.startkey, opts.endkey) > 0) { // can't possibly return any results, startkey > endkey return {docs: []}; } var isDescending = requestDef.sort && typeof requestDef.sort[0] !== 'string' && getValue(requestDef.sort[0]) === 'desc'; if (isDescending) { // either all descending or all ascending opts.descending = true; opts = reverseOptions(opts); } if (!queryPlan.inMemoryFields.length) { // no in-memory filtering necessary, so we can let the // database do the limit/skip for us if ('limit' in requestDef) { opts.limit = requestDef.limit; } if ('skip' in requestDef) { opts.skip = requestDef.skip; } } return Promise.resolve().then(function () { if (indexToUse.name === '_all_docs') { return doAllDocs(db, opts); } else { var signature = indexToSignature(indexToUse); return abstractMapper.query.call(db, signature, opts); } }).then(function (res) { if (opts.inclusive_start === false) { // may have to manually filter the first one, // since couchdb has no true inclusive_start option res.rows = filterInclusiveStart(res.rows, opts.startkey, indexToUse); } if (queryPlan.inMemoryFields.length) { // need to filter some stuff in-memory res.rows = filterInMemoryFields(res.rows, requestDef, queryPlan.inMemoryFields); } return { docs: res.rows.map(function (row) { var doc = row.doc; if (requestDef.fields) { return utils.pick(doc, requestDef.fields); } return doc; }) }; }); }); } module.exports = find; },{"10":10,"12":12,"13":13,"15":15,"17":17,"38":38,"7":7}],12:[function(_dereq_,module,exports){ 'use strict'; var utils = _dereq_(17); var log = utils.log; var localUtils = _dereq_(15); var getKey = localUtils.getKey; var getValue = localUtils.getValue; var getUserFields = localUtils.getUserFields; // couchdb lowest collation value var COLLATE_LO = null; // couchdb highest collation value (TODO: well not really, but close enough amirite) var COLLATE_HI = {"\uffff": {}}; // cou