blob: 0bceca765b4709729f21c85d4d477a88e8e011eb [file] [log] [blame]
import { clone } from 'pouchdb-utils';
// 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 setFieldInDoc(doc, parsedField, value) {
for (var i = 0, len = parsedField.length; i < len-1; i++) {
var elem = parsedField[i];
doc = doc[elem] = {};
}
doc[parsedField[len-1]] = value;
}
function compare(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
// Converts a string in dot notation to an array of its components, with backslash escaping
function parseField(fieldName) {
// fields may be deep (e.g. "foo.bar.baz"), so parse
var fields = [];
var current = '';
for (var i = 0, len = fieldName.length; i < len; i++) {
var ch = fieldName[i];
if (ch === '.') {
if (i > 0 && fieldName[i - 1] === '\\') { // escaped delimiter
current = current.substring(0, current.length - 1) + '.';
} else { // not escaped, so delimiter
fields.push(current);
current = '';
}
} else { // normal character
current += ch;
}
}
fields.push(current);
return fields;
}
var combinationFields = ['$or', '$nor', '$not'];
function isCombinationalField(field) {
return combinationFields.indexOf(field) > -1;
}
function getKey(obj) {
return Object.keys(obj)[0];
}
function getValue(obj) {
return obj[getKey(obj)];
}
// flatten an array of selectors joined by an $and operator
function mergeAndedSelectors(selectors) {
// sort to ensure that e.g. if the user specified
// $and: [{$gt: 'a'}, {$gt: 'b'}], then it's collapsed into
// just {$gt: 'b'}
var res = {};
selectors.forEach(function (selector) {
Object.keys(selector).forEach(function (field) {
var matcher = selector[field];
if (typeof matcher !== 'object') {
matcher = {$eq: matcher};
}
if (isCombinationalField(field)) {
if (matcher instanceof Array) {
res[field] = matcher.map(function (m) {
return mergeAndedSelectors([m]);
});
} else {
res[field] = mergeAndedSelectors([matcher]);
}
} else {
var fieldMatchers = res[field] = res[field] || {};
Object.keys(matcher).forEach(function (operator) {
var value = matcher[operator];
if (operator === '$gt' || operator === '$gte') {
return mergeGtGte(operator, value, fieldMatchers);
} else if (operator === '$lt' || operator === '$lte') {
return mergeLtLte(operator, value, fieldMatchers);
} else if (operator === '$ne') {
return mergeNe(value, fieldMatchers);
} else if (operator === '$eq') {
return mergeEq(value, fieldMatchers);
}
fieldMatchers[operator] = value;
});
}
});
});
return res;
}
// collapse logically equivalent gt/gte values
function mergeGtGte(operator, value, fieldMatchers) {
if (typeof fieldMatchers.$eq !== 'undefined') {
return; // do nothing
}
if (typeof fieldMatchers.$gte !== 'undefined') {
if (operator === '$gte') {
if (value > fieldMatchers.$gte) { // more specificity
fieldMatchers.$gte = value;
}
} else { // operator === '$gt'
if (value >= fieldMatchers.$gte) { // more specificity
delete fieldMatchers.$gte;
fieldMatchers.$gt = value;
}
}
} else if (typeof fieldMatchers.$gt !== 'undefined') {
if (operator === '$gte') {
if (value > fieldMatchers.$gt) { // more specificity
delete fieldMatchers.$gt;
fieldMatchers.$gte = value;
}
} else { // operator === '$gt'
if (value > fieldMatchers.$gt) { // more specificity
fieldMatchers.$gt = value;
}
}
} else {
fieldMatchers[operator] = value;
}
}
// collapse logically equivalent lt/lte values
function mergeLtLte(operator, value, fieldMatchers) {
if (typeof fieldMatchers.$eq !== 'undefined') {
return; // do nothing
}
if (typeof fieldMatchers.$lte !== 'undefined') {
if (operator === '$lte') {
if (value < fieldMatchers.$lte) { // more specificity
fieldMatchers.$lte = value;
}
} else { // operator === '$gt'
if (value <= fieldMatchers.$lte) { // more specificity
delete fieldMatchers.$lte;
fieldMatchers.$lt = value;
}
}
} else if (typeof fieldMatchers.$lt !== 'undefined') {
if (operator === '$lte') {
if (value < fieldMatchers.$lt) { // more specificity
delete fieldMatchers.$lt;
fieldMatchers.$lte = value;
}
} else { // operator === '$gt'
if (value < fieldMatchers.$lt) { // more specificity
fieldMatchers.$lt = value;
}
}
} else {
fieldMatchers[operator] = value;
}
}
// combine $ne values into one array
function mergeNe(value, fieldMatchers) {
if ('$ne' in fieldMatchers) {
// there are many things this could "not" be
fieldMatchers.$ne.push(value);
} else { // doesn't exist yet
fieldMatchers.$ne = [value];
}
}
// add $eq into the mix
function mergeEq(value, fieldMatchers) {
// these all have less specificity than the $eq
// TODO: check for user errors here
delete fieldMatchers.$gt;
delete fieldMatchers.$gte;
delete fieldMatchers.$lt;
delete fieldMatchers.$lte;
delete fieldMatchers.$ne;
fieldMatchers.$eq = value;
}
//
// normalize the selector
//
function massageSelector(input) {
var result = clone(input);
var wasAnded = false;
if ('$and' in result) {
result = mergeAndedSelectors(result['$and']);
wasAnded = true;
}
['$or', '$nor'].forEach(function (orOrNor) {
if (orOrNor in result) {
// message each individual selector
// e.g. {foo: 'bar'} becomes {foo: {$eq: 'bar'}}
result[orOrNor].forEach(function (subSelector) {
var fields = Object.keys(subSelector);
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
var matcher = subSelector[field];
if (typeof matcher !== 'object' || matcher === null) {
subSelector[field] = {$eq: matcher};
}
}
});
}
});
if ('$not' in result) {
//This feels a little like forcing, but it will work for now,
//I would like to come back to this and make the merging of selectors a little more generic
result['$not'] = mergeAndedSelectors([result['$not']]);
}
var fields = Object.keys(result);
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
var matcher = result[field];
if (typeof matcher !== 'object' || matcher === null) {
matcher = {$eq: matcher};
} else if ('$ne' in matcher && !wasAnded) {
// I put these in an array, since there may be more than one
// but in the "mergeAnded" operation, I already take care of that
matcher.$ne = [matcher.$ne];
}
result[field] = matcher;
}
return result;
}
export {
massageSelector,
isCombinationalField,
getKey,
getValue,
getFieldFromDoc,
setFieldInDoc,
compare,
parseField
};