pouchdb-mapreduce
Version:
PouchDB's map/reduce query API as a plugin.
197 lines (173 loc) • 5.56 kB
JavaScript
import vm from 'vm';
import { guardedConsole } from 'pouchdb-utils';
import { BuiltInError, NotFoundError } from 'pouchdb-mapreduce-utils';
import createAbstractMapReduce from 'pouchdb-abstract-mapreduce';
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;
}
// Inside of 'vm' for Node, we need a way to translate a pseudo-error
// back into a real error once it's out of the VM.
function createBuiltInErrorInVm(name) {
return {
builtInError: true,
name
};
}
function convertToTrueError(err) {
return createBuiltInError(err.name);
}
function isBuiltInError(obj) {
return obj && obj.builtInError;
}
// All of this vm hullaballoo is to be able to run arbitrary code in a sandbox
// for security reasons.
function evalFunctionInVm(func, emit) {
return function (arg1, arg2, arg3) {
var code = '(function() {"use strict";' +
'var createBuiltInError = ' + createBuiltInErrorInVm.toString() + ';' +
'var sum = ' + sum.toString() + ';' +
'var log = function () {};' +
'var isArray = Array.isArray;' +
'var toJSON = JSON.parse;' +
'var __emitteds__ = [];' +
'var emit = function (key, value) {__emitteds__.push([key, value]);};' +
'var __result__ = (' +
func.replace(/;\s*$/, '') + ')' + '(' +
JSON.stringify(arg1) + ',' +
JSON.stringify(arg2) + ',' +
JSON.stringify(arg3) + ');' +
'return {result: __result__, emitteds: __emitteds__};' +
'})()';
var output = vm.runInNewContext(code);
output.emitteds.forEach(function (emitted) {
emit(emitted[0], emitted[1]);
});
if (isBuiltInError(output.result)) {
output.result = convertToTrueError(output.result);
}
return output.result;
};
}
var log = guardedConsole.bind(null, 'log');
var toJSON = JSON.parse;
// The "stringify, then execute in a VM" strategy totally breaks Istanbul due
// to missing __coverage global objects. As a solution, export different
// code during coverage testing and during regular execution.
// Note that this doesn't get shipped to consumers because Rollup replaces it
// with rollup-plugin-replace, so false is replaced with `false`
var evalFunc;
/* istanbul ignore else */
{
evalFunc = evalFunctionInVm;
}
var evalFunction = evalFunc;
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 getBuiltIn(reduceFunString) {
if (/^_sum/.test(reduceFunString)) {
return builtInReduce._sum;
} else if (/^_count/.test(reduceFunString)) {
return builtInReduce._count;
} else if (/^_stats/.test(reduceFunString)) {
return builtInReduce._stats;
} else if (/^_/.test(reduceFunString)) {
throw new Error(reduceFunString + ' is not a supported reduce function.');
}
}
function mapper(mapFun, emit) {
// for temp_views one can use emit(doc, emit), see #38
if (typeof mapFun === "function" && mapFun.length === 2) {
var origMap = mapFun;
return function (doc) {
return origMap(doc, emit);
};
} else {
return evalFunction(mapFun.toString(), emit);
}
}
function reducer(reduceFun) {
var reduceFunString = reduceFun.toString();
var builtIn = getBuiltIn(reduceFunString);
if (builtIn) {
return builtIn;
} else {
return evalFunction(reduceFunString);
}
}
function ddocValidator(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);
}
}
var localDocName = 'mrviews';
var abstract = createAbstractMapReduce(localDocName, mapper, reducer, ddocValidator);
function query(fun, opts, callback) {
return abstract.query.call(this, fun, opts, callback);
}
function viewCleanup(callback) {
return abstract.viewCleanup.call(this, callback);
}
var index = {
query,
viewCleanup
};
export default index;