pouchdb-find
Version:
Easy-to-use query language for PouchDB
1,715 lines (1,492 loc) • 151 kB
JavaScript
(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