UNPKG

pouchdb-find

Version:
277 lines (247 loc) 7.79 kB
'use strict'; var evalFunc = require('./evalfunc'); 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 = require('./utils'); var Promise = utils.Promise; 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 BuiltInError(message) { this.status = 500; this.name = 'invalid_value'; this.message = message; this.error = true; try { Error.captureStackTrace(this, BuiltInError); } catch (e) {} } utils.inherits(BuiltInError, 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 createBuiltInError(name) { var message = 'builtin ' + name + ' function requires map values to be numbers' + ' or number arrays'; return new BuiltInError(message); } function sum(values) { var result = 0; for (var i = 0, len = values.length; i < len; i++) { var num = values[i]; if (typeof num !== 'number') { if (Array.isArray(num)) { // lists of numbers are also allowed, sum them separately result = typeof result === 'number' ? [result] : result; for (var j = 0, jLen = num.length; j < jLen; j++) { var jNum = num[j]; if (typeof jNum !== 'number') { throw createBuiltInError('_sum'); } else if (typeof result[j] === 'undefined') { result.push(jNum); } else { result[j] += jNum; } } } else { // not array/number throw createBuiltInError('_sum'); } } else if (typeof result === 'number') { result += num; } else { // add number to array result[0] += num; } } return result; } var builtInReduce = { _sum: function (keys, values) { return sum(values); }, _count: function (keys, values) { return values.length; }, _stats: function (keys, values) { // no need to implement rereduce=true, because Pouch // will never call it function sumsqr(values) { var _sumsqr = 0; for (var i = 0, len = values.length; i < len; i++) { var num = values[i]; _sumsqr += (num * num); } return _sumsqr; } return { sum : sum(values), min : Math.min.apply(null, values), max : Math.max.apply(null, values), count : values.length, sumsqr : sumsqr(values) }; } }; 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 httpQueryPromised(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('endkey', 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 request 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 = JSON.stringify({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 }); } // We are using a temporary view, terrible for performance but 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 }); } function httpViewCleanup(db) { return db.request({ method: 'POST', url: '_view_cleanup' }); } function httpQuery(db, fun, opts, callback) { 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 httpQueryPromised(db, fun, opts); }); utils.promisedCallback(promise, callback); return promise; } var abstractMapReduce = require('../../lib/abstract-mapreduce'); var abstract = abstractMapReduce({ name: 'mrviews', mapper: function (inputMapFun, emit) { var mapFun; if (typeof inputMapFun === "function" && inputMapFun.length === 2) { var origMap = inputMapFun; mapFun = function (doc) { return origMap(doc, emit); }; } else { mapFun = evalFunc(inputMapFun.toString(), emit, sum, log, Array.isArray, JSON.parse); } return mapFun; }, reducer: function (inputReduceFun) { var reduceFun; if (builtInReduce[inputReduceFun]) { reduceFun = builtInReduce[inputReduceFun]; } else { reduceFun = evalFunc( inputReduceFun.toString(), null, sum, log, Array.isArray, JSON.parse); } return reduceFun; }, ddocValidator: function (ddoc, viewName) { var fun = ddoc.views && ddoc.views[viewName]; if (typeof fun.map !== 'string') { throw new NotFoundError('ddoc ' + ddoc._id + ' has no string view named ' + viewName + ', instead found object of type: ' + typeof fun.map); } } }); exports.query = function (fun, opts, callback) { var db = this; if (db.type() === 'http') { return httpQuery(db, fun, opts, callback); } return abstract.query.apply(db, [fun, opts, callback]); }; exports.viewCleanup = utils.callbackify(function () { var db = this; if (db.type() === 'http') { return httpViewCleanup(db); } return abstract.viewCleanup.apply(db); });