blob: 04771697139dde8ed18dba56acfc3ae11cfae567 [file] [log] [blame]
'use strict';
import {
DOC_STORE,
idbError,
rawIndexFields,
naturalIndexName
} from './util';
import {
IDB_NULL,
IDB_TRUE,
IDB_FALSE,
} from './rewrite';
// Adapted from
// https://github.com/pouchdb/pouchdb/blob/master/packages/node_modules/pouchdb-find/src/adapters/local/find/query-planner.js#L20-L24
// This could change / improve in the future?
var COUCH_COLLATE_LO = null;
var COUCH_COLLATE_HI = '\uffff'; // actually used as {"\uffff": {}}
// Adapted from: https://www.w3.org/TR/IndexedDB/#compare-two-keys
// Importantly, *there is no upper bound possible* in idb. The ideal data
// structure an infintely deep array:
// var IDB_COLLATE_HI = []; IDB_COLLATE_HI.push(IDB_COLLATE_HI)
// But IDBKeyRange is not a fan of shenanigans, so I've just gone with 12 layers
// because it looks nice and surely that's enough!
var IDB_COLLATE_LO = Number.NEGATIVE_INFINITY;
var IDB_COLLATE_HI = [[[[[[[[[[[[]]]]]]]]]]]];
//
// TODO: this should be made offical somewhere and used by AllDocs / get /
// changes etc as well.
//
function externaliseRecord(idbDoc) {
var doc = idbDoc.revs[idbDoc.rev].data;
doc._id = idbDoc.id;
doc._rev = idbDoc.rev;
if (idbDoc.deleted) {
doc._deleted = true;
}
return doc;
}
/**
* Generates a keyrange based on the opts passed to query
*
* The first key is always 0, as that's how we're filtering out deleted entries.
*/
function generateKeyRange(opts) {
function defined(obj, k) {
return obj[k] !== void 0;
}
// Converts a valid CouchDB key into a valid IndexedDB one
function convert(key, exact) {
// The first item in every native index is doc.deleted, and we always want
// to only search documents that are not deleted.
// "foo" -> [0, "foo"]
var filterDeleted = [0].concat(key);
return filterDeleted.map(function (k) {
// null, true and false are not indexable by indexeddb. When we write
// these values we convert them to these constants, and so when we
// query for them we need to convert the query also.
if (k === null && exact) {
// for non-exact queries we treat null as a collate property
// see `if (!exact)` block below
return IDB_NULL;
} else if (k === true) {
return IDB_TRUE;
} else if (k === false) {
return IDB_FALSE;
}
if (!exact) {
// We get passed CouchDB's collate low and high values, so for non-exact
// ranged queries we're going to convert them to our IDB equivalents
if (k === COUCH_COLLATE_LO) {
return IDB_COLLATE_LO;
} else if (k.hasOwnProperty(COUCH_COLLATE_HI)) {
return IDB_COLLATE_HI;
}
}
return k;
});
}
// CouchDB and so PouchdB defaults to true. We need to make this explicit as
// we invert these later for IndexedDB.
if (!defined(opts, 'inclusive_end')) {
opts.inclusive_end = true;
}
if (!defined(opts, 'inclusive_start')) {
opts.inclusive_start = true;
}
if (opts.descending) {
// Flip before generating. We'll check descending again later when performing
// an index request
var realEndkey = opts.startkey,
realInclusiveEnd = opts.inclusive_start;
opts.startkey = opts.endkey;
opts.endkey = realEndkey;
opts.inclusive_start = opts.inclusive_end;
opts.inclusive_end = realInclusiveEnd;
}
try {
if (defined(opts, 'key')) {
return IDBKeyRange.only(convert(opts.key, true));
}
if (defined(opts, 'startkey') && !defined(opts, 'endkey')) {
return IDBKeyRange.lowerBound(convert(opts.startkey), !opts.inclusive_start);
}
if (!defined(opts, 'startkey') && defined(opts, 'endkey')) {
return IDBKeyRange.upperBound(convert(opts.endkey), !opts.inclusive_end);
}
if (defined(opts, 'startkey') && defined(opts, 'endkey')) {
return IDBKeyRange.bound(
convert(opts.startkey), convert(opts.endkey),
!opts.inclusive_start, !opts.inclusive_end
);
}
return IDBKeyRange.only([0]);
} catch (err) {
console.error('Could not generate keyRange', err, opts);
throw Error('Could not generate key range with ' + JSON.stringify(opts));
}
}
function getIndexHandle(pdb, fields, reject) {
var indexName = naturalIndexName(fields);
return new Promise(function (resolve) {
pdb._openTransactionSafely([DOC_STORE], 'readonly', function (err, txn) {
if (err) {
return idbError(reject)(err);
}
txn.onabort = idbError(reject);
txn.ontimeout = idbError(reject);
var existingIndexNames = Array.from(txn.objectStore(DOC_STORE).indexNames);
if (existingIndexNames.indexOf(indexName) === -1) {
// The index is missing, force a db restart and try again
pdb._freshen()
.then(function () { return getIndexHandle(pdb, fields, reject); })
.then(resolve);
} else {
resolve(txn.objectStore(DOC_STORE).index(indexName));
}
});
});
}
// In theory we should return something like the doc example below, but find
// only needs rows: [{doc: {...}}], so I think we can just not bother for now
// {
// "offset" : 0,
// "rows": [{
// "id": "doc3",
// "key": "Lisa Says",
// "value": null,
// "doc": {
// "_id": "doc3",
// "_rev": "1-z",
// "title": "Lisa Says"
// }
// }],
// "total_rows" : 4
// }
function query(idb, signature, opts) {
// At this stage, in the current implementation, find has already gone through
// and determined if the index already exists from PouchDB's perspective (eg
// there is a design doc for it).
//
// If we find that the index doesn't exist this means we have to close and
// re-open the DB to correct indexes before proceeding, at which point the
// index should exist.
var pdb = this;
// Assumption, there will be only one /, between the design document name
// and the view name.
var parts = signature.split('/');
return new Promise(function (resolve, reject) {
pdb.get('_design/' + parts[0]).then(function (ddoc) {
var fields = rawIndexFields(ddoc, parts[1]);
if (!fields) {
throw new Error('ddoc ' + ddoc._id +' with view ' + parts[1] +
' does not have map.options.def.fields defined.');
}
var skip = opts.skip;
var limit = Number.isInteger(opts.limit) && opts.limit;
return getIndexHandle(pdb, fields, reject)
.then(function (indexHandle) {
var keyRange = generateKeyRange(opts);
var req = indexHandle.openCursor(keyRange, opts.descending ? 'prev' : 'next');
var rows = [];
req.onerror = idbError(reject);
req.onsuccess = function (e) {
var cursor = e.target.result;
if (!cursor || limit === 0) {
return resolve({
rows: rows
});
}
if (skip) {
cursor.advance(skip);
skip = false;
return;
}
if (limit) {
limit = limit - 1;
}
rows.push({doc: externaliseRecord(cursor.value)});
cursor.continue();
};
});
})
.catch(reject);
});
}
function viewCleanup() {
// I'm not sure we have to do anything here.
//
// One option is to just close and re-open the DB, which performs the same
// action. The only reason you'd want to call this is if you deleted a bunch
// of indexes and wanted the space back immediately.
//
// Otherwise index cleanup happens when:
// - A DB is opened
// - A find query is performed against an index that doesn't exist but should
return Promise.resolve();
}
export { query, viewCleanup };