| import { collate } from 'pouchdb-collate'; |
| import { clone } from 'pouchdb-utils'; |
| import { getKey, getValue, massageSelector, parseField, getFieldFromDoc } from 'pouchdb-selector-core'; |
| |
| // normalize the "sort" value |
| function massageSort(sort) { |
| if (!Array.isArray(sort)) { |
| throw new Error('invalid sort json - should be an array'); |
| } |
| return sort.map(function (sorting) { |
| if (typeof sorting === 'string') { |
| const obj = {}; |
| obj[sorting] = 'asc'; |
| return obj; |
| } else { |
| return sorting; |
| } |
| }); |
| } |
| |
| const ddocIdPrefix = /^_design\//; |
| function massageUseIndex(useIndex) { |
| let cleanedUseIndex = []; |
| if (typeof useIndex === 'string') { |
| cleanedUseIndex.push(useIndex); |
| } else { |
| cleanedUseIndex = useIndex; |
| } |
| |
| return cleanedUseIndex.map(function (name) { |
| return name.replace(ddocIdPrefix, ''); |
| }); |
| } |
| |
| function massageIndexDef(indexDef) { |
| indexDef.fields = indexDef.fields.map(function (field) { |
| if (typeof field === 'string') { |
| const obj = {}; |
| obj[field] = 'asc'; |
| return obj; |
| } |
| return field; |
| }); |
| if (indexDef.partial_filter_selector) { |
| indexDef.partial_filter_selector = massageSelector( |
| indexDef.partial_filter_selector |
| ); |
| } |
| return indexDef; |
| } |
| |
| function getKeyFromDoc(doc, index) { |
| return index.def.fields.map((obj) => { |
| const field = getKey(obj); |
| return getFieldFromDoc(doc, parseField(field)); |
| }); |
| } |
| |
| // have to do this manually because REASONS. I don't know why |
| // CouchDB didn't implement inclusive_start |
| function filterInclusiveStart(rows, targetValue, index) { |
| const indexFields = index.def.fields; |
| let startAt = 0; |
| for (const row of rows) { |
| // shave off any docs at the beginning that are <= the |
| // target value |
| |
| let docKey = getKeyFromDoc(row.doc, index); |
| if (indexFields.length === 1) { |
| docKey = docKey[0]; // only one field, not multi-field |
| } else { // more than one field in index |
| // in the case where e.g. the user is searching {$gt: {a: 1}} |
| // but the index is [a, b], then we need to shorten the doc key |
| while (docKey.length > targetValue.length) { |
| docKey.pop(); |
| } |
| } |
| //ABS as we just looking for values that don't match |
| if (Math.abs(collate(docKey, targetValue)) > 0) { |
| // no need to filter any further; we're past the key |
| break; |
| } |
| ++startAt; |
| } |
| return startAt > 0 ? rows.slice(startAt) : rows; |
| } |
| |
| function reverseOptions(opts) { |
| const newOpts = clone(opts); |
| delete newOpts.startkey; |
| delete newOpts.endkey; |
| delete newOpts.inclusive_start; |
| delete newOpts.inclusive_end; |
| |
| if ('endkey' in opts) { |
| newOpts.startkey = opts.endkey; |
| } |
| if ('startkey' in opts) { |
| newOpts.endkey = opts.startkey; |
| } |
| if ('inclusive_start' in opts) { |
| newOpts.inclusive_end = opts.inclusive_start; |
| } |
| if ('inclusive_end' in opts) { |
| newOpts.inclusive_start = opts.inclusive_end; |
| } |
| return newOpts; |
| } |
| |
| function validateIndex(index) { |
| const ascFields = index.fields.filter(function (field) { |
| return getValue(field) === 'asc'; |
| }); |
| if (ascFields.length !== 0 && ascFields.length !== index.fields.length) { |
| throw new Error('unsupported mixed sorting'); |
| } |
| } |
| |
| function validateSort(requestDef, index) { |
| if (index.defaultUsed && requestDef.sort) { |
| const noneIdSorts = requestDef.sort.filter(function (sortItem) { |
| return Object.keys(sortItem)[0] !== '_id'; |
| }).map(function (sortItem) { |
| return Object.keys(sortItem)[0]; |
| }); |
| |
| if (noneIdSorts.length > 0) { |
| throw new Error('Cannot sort on field(s) "' + noneIdSorts.join(',') + |
| '" when using the default index'); |
| } |
| } |
| |
| if (index.defaultUsed) { |
| return; |
| } |
| } |
| |
| function validateFindRequest(requestDef) { |
| if (typeof requestDef.selector !== 'object') { |
| throw new Error('you must provide a selector when you find()'); |
| } |
| |
| /*var selectors = requestDef.selector['$and'] || [requestDef.selector]; |
| for (var i = 0; i < selectors.length; i++) { |
| var selector = selectors[i]; |
| var keys = Object.keys(selector); |
| if (keys.length === 0) { |
| throw new Error('invalid empty selector'); |
| } |
| //var selection = selector[keys[0]]; |
| /*if (Object.keys(selection).length !== 1) { |
| throw new Error('invalid selector: ' + JSON.stringify(selection) + |
| ' - it must have exactly one key/value'); |
| } |
| }*/ |
| } |
| |
| // determine the maximum number of fields |
| // we're going to need to query, e.g. if the user |
| // has selection ['a'] and sorting ['a', 'b'], then we |
| // need to use the longer of the two: ['a', 'b'] |
| function getUserFields(selector, sort) { |
| const selectorFields = Object.keys(selector); |
| const sortFields = sort ? sort.map(getKey) : []; |
| let userFields; |
| if (selectorFields.length >= sortFields.length) { |
| userFields = selectorFields; |
| } else { |
| userFields = sortFields; |
| } |
| |
| if (sortFields.length === 0) { |
| return { |
| fields: userFields |
| }; |
| } |
| |
| // sort according to the user's preferred sorting |
| userFields = userFields.sort(function (left, right) { |
| let leftIdx = sortFields.indexOf(left); |
| if (leftIdx === -1) { |
| leftIdx = Number.MAX_VALUE; |
| } |
| let rightIdx = sortFields.indexOf(right); |
| if (rightIdx === -1) { |
| rightIdx = Number.MAX_VALUE; |
| } |
| return leftIdx < rightIdx ? -1 : leftIdx > rightIdx ? 1 : 0; |
| }); |
| |
| return { |
| fields: userFields, |
| sortOrder: sort.map(getKey) |
| }; |
| } |
| |
| export { |
| massageSort, |
| validateIndex, |
| validateFindRequest, |
| validateSort, |
| reverseOptions, |
| filterInclusiveStart, |
| massageIndexDef, |
| getUserFields, |
| massageUseIndex |
| }; |