rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
596 lines (565 loc) • 20.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RxQueryBase = void 0;
exports._getDefaultQuery = _getDefaultQuery;
exports.createRxQuery = createRxQuery;
exports.isFindOneByIdQuery = isFindOneByIdQuery;
exports.isRxQuery = isRxQuery;
exports.queryCollection = queryCollection;
exports.tunnelQueryCache = tunnelQueryCache;
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _rxjs = require("rxjs");
var _operators = require("rxjs/operators");
var _index = require("./plugins/utils/index.js");
var _rxError = require("./rx-error.js");
var _hooks = require("./hooks.js");
var _eventReduce = require("./event-reduce.js");
var _queryCache = require("./query-cache.js");
var _rxQueryHelper = require("./rx-query-helper.js");
var _rxQuerySingleResult = require("./rx-query-single-result.js");
var _queryCount = 0;
var newQueryID = function () {
return ++_queryCount;
};
var RxQueryBase = exports.RxQueryBase = /*#__PURE__*/function () {
/**
* Some stats then are used for debugging and cache replacement policies
*/
// used in the query-cache to determine if the RxQuery can be cleaned up.
// used to count the subscribers to the query
/**
* Contains the current result state
* or null if query has not run yet.
*/
function RxQueryBase(op, mangoQuery, collection,
// used by some plugins
other = {}) {
this.id = newQueryID();
this._execOverDatabaseCount = 0;
this._creationTime = (0, _index.now)();
this._lastEnsureEqual = 0;
this.uncached = false;
this.refCount$ = new _rxjs.BehaviorSubject(null);
this._result = null;
this._latestChangeEvent = -1;
this._ensureEqualQueue = _index.PROMISE_RESOLVE_FALSE;
this.op = op;
this.mangoQuery = mangoQuery;
this.collection = collection;
this.other = other;
if (!mangoQuery) {
this.mangoQuery = _getDefaultQuery();
}
this.isFindOneByIdQuery = isFindOneByIdQuery(this.collection.schema.primaryPath, mangoQuery);
}
var _proto = RxQueryBase.prototype;
/**
* Returns an observable that emits the results
* This should behave like an rxjs-BehaviorSubject which means:
* - Emit the current result-set on subscribe
* - Emit the new result-set when an RxChangeEvent comes in
* - Do not emit anything before the first result-set was created (no null)
*/
/**
* set the new result-data as result-docs of the query
* @param newResultData json-docs that were received from the storage
*/
_proto._setResultData = function _setResultData(newResultData) {
if (typeof newResultData === 'undefined') {
throw (0, _rxError.newRxError)('QU18', {
database: this.collection.database.name,
collection: this.collection.name
});
}
if (typeof newResultData === 'number') {
this._result = new _rxQuerySingleResult.RxQuerySingleResult(this, [], newResultData);
return;
} else if (newResultData instanceof Map) {
newResultData = Array.from(newResultData.values());
}
var newQueryResult = new _rxQuerySingleResult.RxQuerySingleResult(this, newResultData, newResultData.length);
this._result = newQueryResult;
}
/**
* executes the query on the database
* @return results-array with document-data
*/;
_proto._execOverDatabase = async function _execOverDatabase() {
this._execOverDatabaseCount = this._execOverDatabaseCount + 1;
if (this.op === 'count') {
var preparedQuery = this.getPreparedQuery();
var result = await this.collection.storageInstance.count(preparedQuery);
if (result.mode === 'slow' && !this.collection.database.allowSlowCount) {
throw (0, _rxError.newRxError)('QU14', {
collection: this.collection,
queryObj: this.mangoQuery
});
} else {
return result.count;
}
}
if (this.op === 'findByIds') {
var ids = (0, _index.ensureNotFalsy)(this.mangoQuery.selector)[this.collection.schema.primaryPath].$in;
var ret = new Map();
var mustBeQueried = [];
// first try to fill from docCache
ids.forEach(id => {
var docData = this.collection._docCache.getLatestDocumentDataIfExists(id);
if (docData) {
if (!docData._deleted) {
var doc = this.collection._docCache.getCachedRxDocument(docData);
ret.set(id, doc);
}
} else {
mustBeQueried.push(id);
}
});
// everything which was not in docCache must be fetched from the storage
if (mustBeQueried.length > 0) {
var docs = await this.collection.storageInstance.findDocumentsById(mustBeQueried, false);
docs.forEach(docData => {
var doc = this.collection._docCache.getCachedRxDocument(docData);
ret.set(doc.primary, doc);
});
}
return ret;
}
var docsPromise = queryCollection(this);
return docsPromise.then(docs => {
return docs;
});
}
/**
* Execute the query
* To have an easier implementations,
* just subscribe and use the first result
*/;
_proto.exec = async function exec(throwIfMissing) {
if (throwIfMissing && this.op !== 'findOne') {
throw (0, _rxError.newRxError)('QU9', {
collection: this.collection.name,
query: this.mangoQuery,
op: this.op
});
}
/**
* run _ensureEqual() here,
* this will make sure that errors in the query which throw inside of the RxStorage,
* will be thrown at this execution context and not in the background.
*/
await _ensureEqual(this);
var useResult = (0, _index.ensureNotFalsy)(this._result);
return useResult.getValue(throwIfMissing);
}
/**
* cached call to get the queryMatcher
* @overwrites itself with the actual value
*/;
/**
* returns a string that is used for equal-comparisons
* @overwrites itself with the actual value
*/
_proto.toString = function toString() {
var stringObj = (0, _index.sortObject)({
op: this.op,
query: this.mangoQuery,
other: this.other
}, true);
var value = JSON.stringify(stringObj);
this.toString = () => value;
return value;
}
/**
* returns the prepared query
* which can be send to the storage instance to query for documents.
* @overwrites itself with the actual value.
*/;
_proto.getPreparedQuery = function getPreparedQuery() {
var hookInput = {
rxQuery: this,
// can be mutated by the hooks so we have to deep clone first.
mangoQuery: (0, _rxQueryHelper.normalizeMangoQuery)(this.collection.schema.jsonSchema, this.mangoQuery)
};
hookInput.mangoQuery.selector._deleted = {
$eq: false
};
if (hookInput.mangoQuery.index) {
hookInput.mangoQuery.index.unshift('_deleted');
}
(0, _hooks.runPluginHooks)('prePrepareQuery', hookInput);
var value = (0, _rxQueryHelper.prepareQuery)(this.collection.schema.jsonSchema, hookInput.mangoQuery);
this.getPreparedQuery = () => value;
return value;
}
/**
* returns true if the document matches the query,
* does not use the 'skip' and 'limit'
*/;
_proto.doesDocumentDataMatch = function doesDocumentDataMatch(docData) {
// if doc is deleted, it cannot match
if (docData._deleted) {
return false;
}
return this.queryMatcher(docData);
}
/**
* deletes all found documents
* @return promise with deleted documents
*/;
_proto.remove = async function remove() {
var docs = await this.exec();
if (Array.isArray(docs)) {
var result = await this.collection.bulkRemove(docs);
if (result.error.length > 0) {
throw (0, _rxError.rxStorageWriteErrorToRxError)(result.error[0]);
} else {
return result.success;
}
} else {
return docs.remove();
}
};
_proto.incrementalRemove = function incrementalRemove() {
return (0, _rxQueryHelper.runQueryUpdateFunction)(this.asRxQuery, doc => doc.incrementalRemove());
}
/**
* helper function to transform RxQueryBase to RxQuery type
*/;
/**
* updates all found documents
* @overwritten by plugin (optional)
*/
_proto.update = function update(_updateObj) {
throw (0, _index.pluginMissing)('update');
};
_proto.patch = function patch(_patch) {
return (0, _rxQueryHelper.runQueryUpdateFunction)(this.asRxQuery, doc => doc.patch(_patch));
};
_proto.incrementalPatch = function incrementalPatch(patch) {
return (0, _rxQueryHelper.runQueryUpdateFunction)(this.asRxQuery, doc => doc.incrementalPatch(patch));
};
_proto.modify = function modify(mutationFunction) {
return (0, _rxQueryHelper.runQueryUpdateFunction)(this.asRxQuery, doc => doc.modify(mutationFunction));
};
_proto.incrementalModify = function incrementalModify(mutationFunction) {
return (0, _rxQueryHelper.runQueryUpdateFunction)(this.asRxQuery, doc => doc.incrementalModify(mutationFunction));
}
// we only set some methods of query-builder here
// because the others depend on these ones
;
_proto.where = function where(_queryObj) {
throw (0, _index.pluginMissing)('query-builder');
};
_proto.sort = function sort(_params) {
throw (0, _index.pluginMissing)('query-builder');
};
_proto.skip = function skip(_amount) {
throw (0, _index.pluginMissing)('query-builder');
};
_proto.limit = function limit(_amount) {
throw (0, _index.pluginMissing)('query-builder');
};
return (0, _createClass2.default)(RxQueryBase, [{
key: "$",
get: function () {
if (!this._$) {
var results$ = this.collection.eventBulks$.pipe(
/**
* Performance shortcut.
* Changes to local documents are not relevant for the query.
*/
(0, _operators.filter)(bulk => !bulk.isLocal),
/**
* Start once to ensure the querying also starts
* when there where no changes.
*/
(0, _operators.startWith)(null),
// ensure query results are up to date.
(0, _operators.mergeMap)(() => _ensureEqual(this)),
// use the current result set, written by _ensureEqual().
(0, _operators.map)(() => this._result),
// do not run stuff above for each new subscriber, only once.
(0, _operators.shareReplay)(_index.RXJS_SHARE_REPLAY_DEFAULTS),
// do not proceed if result set has not changed.
(0, _operators.distinctUntilChanged)((prev, curr) => {
if (prev && prev.time === (0, _index.ensureNotFalsy)(curr).time) {
return true;
} else {
return false;
}
}), (0, _operators.filter)(result => !!result),
/**
* Map the result set to a single RxDocument or an array,
* depending on query type
*/
(0, _operators.map)(result => {
return (0, _index.ensureNotFalsy)(result).getValue();
}));
this._$ = (0, _rxjs.merge)(results$,
/**
* Also add the refCount$ to the query observable
* to allow us to count the amount of subscribers.
*/
this.refCount$.pipe((0, _operators.filter)(() => false)));
}
return this._$;
}
}, {
key: "$$",
get: function () {
var reactivity = this.collection.database.getReactivityFactory();
return reactivity.fromObservable(this.$, undefined, this.collection.database);
}
// stores the changeEvent-number of the last handled change-event
/**
* ensures that the exec-runs
* are not run in parallel
*/
}, {
key: "queryMatcher",
get: function () {
var schema = this.collection.schema.jsonSchema;
var normalizedQuery = (0, _rxQueryHelper.normalizeMangoQuery)(this.collection.schema.jsonSchema, this.mangoQuery);
return (0, _index.overwriteGetterForCaching)(this, 'queryMatcher', (0, _rxQueryHelper.getQueryMatcher)(schema, normalizedQuery));
}
}, {
key: "asRxQuery",
get: function () {
return this;
}
}]);
}();
function _getDefaultQuery() {
return {
selector: {}
};
}
/**
* run this query through the QueryCache
*/
function tunnelQueryCache(rxQuery) {
return rxQuery.collection._queryCache.getByQuery(rxQuery);
}
function createRxQuery(op, queryObj, collection, other) {
(0, _hooks.runPluginHooks)('preCreateRxQuery', {
op,
queryObj,
collection,
other
});
var ret = new RxQueryBase(op, queryObj, collection, other);
// ensure when created with same params, only one is created
ret = tunnelQueryCache(ret);
(0, _queryCache.triggerCacheReplacement)(collection);
return ret;
}
/**
* Check if the current results-state is in sync with the database
* which means that no write event happened since the last run.
* @return false if not which means it should re-execute
*/
function _isResultsInSync(rxQuery) {
var currentLatestEventNumber = rxQuery.asRxQuery.collection._changeEventBuffer.getCounter();
if (rxQuery._latestChangeEvent >= currentLatestEventNumber) {
return true;
} else {
return false;
}
}
/**
* wraps __ensureEqual()
* to ensure it does not run in parallel
* @return true if has changed, false if not
*/
async function _ensureEqual(rxQuery) {
if (rxQuery.collection.awaitBeforeReads.size > 0) {
await Promise.all(Array.from(rxQuery.collection.awaitBeforeReads).map(fn => fn()));
}
// Optimisation shortcut
if (rxQuery.collection.database.closed || _isResultsInSync(rxQuery)) {
return false;
}
rxQuery._ensureEqualQueue = rxQuery._ensureEqualQueue.then(() => __ensureEqual(rxQuery));
return rxQuery._ensureEqualQueue;
}
/**
* ensures that the results of this query is equal to the results which a query over the database would give
* @return true if results have changed
*/
function __ensureEqual(rxQuery) {
rxQuery._lastEnsureEqual = (0, _index.now)();
/**
* Optimisation shortcuts
*/
if (
// db is closed
rxQuery.collection.database.closed ||
// nothing happened since last run
_isResultsInSync(rxQuery)) {
return _index.PROMISE_RESOLVE_FALSE;
}
var ret = false;
var mustReExec = false; // if this becomes true, a whole execution over the database is made
if (rxQuery._latestChangeEvent === -1) {
// have not executed yet -> must run
mustReExec = true;
}
/**
* try to use EventReduce to calculate the new results
*/
if (!mustReExec) {
var missedChangeEvents = rxQuery.asRxQuery.collection._changeEventBuffer.getFrom(rxQuery._latestChangeEvent + 1);
if (missedChangeEvents === null) {
// changeEventBuffer is of bounds -> we must re-execute over the database
mustReExec = true;
} else {
rxQuery._latestChangeEvent = rxQuery.asRxQuery.collection._changeEventBuffer.getCounter();
var runChangeEvents = rxQuery.asRxQuery.collection._changeEventBuffer.reduceByLastOfDoc(missedChangeEvents);
if (rxQuery.op === 'count') {
// 'count' query
var previousCount = (0, _index.ensureNotFalsy)(rxQuery._result).count;
var newCount = previousCount;
runChangeEvents.forEach(cE => {
var didMatchBefore = cE.previousDocumentData && rxQuery.doesDocumentDataMatch(cE.previousDocumentData);
var doesMatchNow = rxQuery.doesDocumentDataMatch(cE.documentData);
if (!didMatchBefore && doesMatchNow) {
newCount++;
}
if (didMatchBefore && !doesMatchNow) {
newCount--;
}
});
if (newCount !== previousCount) {
ret = true; // true because results changed
rxQuery._setResultData(newCount);
}
} else {
// 'find' or 'findOne' query
var eventReduceResult = (0, _eventReduce.calculateNewResults)(rxQuery, runChangeEvents);
if (eventReduceResult.runFullQueryAgain) {
// could not calculate the new results, execute must be done
mustReExec = true;
} else if (eventReduceResult.changed) {
// we got the new results, we do not have to re-execute, mustReExec stays false
ret = true; // true because results changed
rxQuery._setResultData(eventReduceResult.newResults);
}
}
}
}
// oh no we have to re-execute the whole query over the database
if (mustReExec) {
return rxQuery._execOverDatabase().then(newResultData => {
/**
* The RxStorage is defined to always first emit events and then return
* on bulkWrite() calls. So here we have to use the counter AFTER the execOverDatabase()
* has been run, not the one from before.
*/
rxQuery._latestChangeEvent = rxQuery.collection._changeEventBuffer.getCounter();
// A count query needs a different has-changed check.
if (typeof newResultData === 'number') {
if (!rxQuery._result || newResultData !== rxQuery._result.count) {
ret = true;
rxQuery._setResultData(newResultData);
}
return ret;
}
if (!rxQuery._result || !(0, _index.areRxDocumentArraysEqual)(rxQuery.collection.schema.primaryPath, newResultData, rxQuery._result.docsData)) {
ret = true; // true because results changed
rxQuery._setResultData(newResultData);
}
return ret;
});
}
return Promise.resolve(ret); // true if results have changed
}
/**
* Runs the query over the storage instance
* of the collection.
* Does some optimizations to ensure findById is used
* when specific queries are used.
*/
async function queryCollection(rxQuery) {
var docs = [];
var collection = rxQuery.collection;
/**
* Optimizations shortcut.
* If query is find-one-document-by-id,
* then we do not have to use the slow query() method
* but instead can use findDocumentsById()
*/
if (rxQuery.isFindOneByIdQuery) {
if (Array.isArray(rxQuery.isFindOneByIdQuery)) {
var docIds = rxQuery.isFindOneByIdQuery;
docIds = docIds.filter(docId => {
// first try to fill from docCache
var docData = rxQuery.collection._docCache.getLatestDocumentDataIfExists(docId);
if (docData) {
if (!docData._deleted) {
docs.push(docData);
}
return false;
} else {
return true;
}
});
// otherwise get from storage
if (docIds.length > 0) {
var docsFromStorage = await collection.storageInstance.findDocumentsById(docIds, false);
(0, _index.appendToArray)(docs, docsFromStorage);
}
} else {
var docId = rxQuery.isFindOneByIdQuery;
// first try to fill from docCache
var docData = rxQuery.collection._docCache.getLatestDocumentDataIfExists(docId);
if (!docData) {
// otherwise get from storage
var fromStorageList = await collection.storageInstance.findDocumentsById([docId], false);
if (fromStorageList[0]) {
docData = fromStorageList[0];
}
}
if (docData && !docData._deleted) {
docs.push(docData);
}
}
} else {
var preparedQuery = rxQuery.getPreparedQuery();
var queryResult = await collection.storageInstance.query(preparedQuery);
docs = queryResult.documents;
}
return docs;
}
/**
* Returns true if the given query
* selects exactly one document by its id.
* Used to optimize performance because these kind of
* queries do not have to run over an index and can use get-by-id instead.
* Returns false if no query of that kind.
* Returns the document id otherwise.
*/
function isFindOneByIdQuery(primaryPath, query) {
// must have exactly one operator which must be $eq || $in
if (!query.skip && query.selector && Object.keys(query.selector).length === 1 && query.selector[primaryPath]) {
var value = query.selector[primaryPath];
if (typeof value === 'string') {
return value;
} else if (Object.keys(value).length === 1 && typeof value.$eq === 'string') {
return value.$eq;
}
// same with $in string arrays
if (Object.keys(value).length === 1 && Array.isArray(value.$eq) &&
// must only contain strings
!value.$eq.find(r => typeof r !== 'string')) {
return value.$eq;
}
}
return false;
}
function isRxQuery(obj) {
return obj instanceof RxQueryBase;
}
//# sourceMappingURL=rx-query.js.map