import { LOGICAL_OPERATORS } from 'nxdb-old/src/query-planner'; import { getPrimaryFieldOfPrimaryKey } from 'nxdb-old/src/rx-schema-helper'; import type { DeterministicSortComparator, FilledMangoQuery, MangoQuery, MangoQuerySortDirection, QueryMatcher, RxDocumentData, RxJsonSchema } from 'nxdb-old/src/types'; import { clone, firstPropertyNameOfObject, toArray, isMaybeReadonlyArray, parseRegex, flatClone, objectPathMonad, ObjectPathMonadFunction } from 'nxdb-old/src/plugins/utils'; import { compare as mingoSortComparator } from 'mingo/util'; import { newRxError } from 'nxdb-old/src/rx-error'; import { getMingoQuery } from 'nxdb-old/src/rx-query-mingo'; /** * Normalize the query to ensure we have all fields set * and queries that represent the same query logic are detected as equal by the caching. */ export function normalizeMangoQuery( schema: RxJsonSchema>, mangoQuery: MangoQuery ): FilledMangoQuery { const primaryKey: string = getPrimaryFieldOfPrimaryKey(schema.primaryKey); mangoQuery = flatClone(mangoQuery); // regex normalization must run before deep clone because deep clone cannot clone RegExp if (mangoQuery.selector) { mangoQuery.selector = normalizeQueryRegex(mangoQuery.selector); } const normalizedMangoQuery: FilledMangoQuery = clone(mangoQuery) as any; if (typeof normalizedMangoQuery.skip !== 'number') { normalizedMangoQuery.skip = 0; } if (!normalizedMangoQuery.selector) { normalizedMangoQuery.selector = {}; } else { normalizedMangoQuery.selector = normalizedMangoQuery.selector; /** * In mango query, it is possible to have an * equals comparison by directly assigning a value * to a property, without the '$eq' operator. * Like: * selector: { * foo: 'bar' * } * For normalization, we have to normalize this * so our checks can perform properly. * * * TODO this must work recursive with nested queries that * contain multiple selectors via $and or $or etc. */ Object .entries(normalizedMangoQuery.selector) .forEach(([field, matcher]) => { if (typeof matcher !== 'object' || matcher === null) { (normalizedMangoQuery as any).selector[field] = { $eq: matcher }; } }); } /** * Ensure that if an index is specified, * the primaryKey is inside of it. */ if (normalizedMangoQuery.index) { const indexAr = toArray(normalizedMangoQuery.index); if (!indexAr.includes(primaryKey)) { indexAr.push(primaryKey); } normalizedMangoQuery.index = indexAr; } /** * To ensure a deterministic sorting, * we have to ensure the primary key is always part * of the sort query. * Primary sorting is added as last sort parameter, * similar to how we add the primary key to indexes that do not have it. * */ if (!normalizedMangoQuery.sort) { /** * If no sort is given at all, * we can assume that the user does not care about sort order at al. * * we cannot just use the primary key as sort parameter * because it would likely cause the query to run over the primary key index * which has a bad performance in most cases. */ if (normalizedMangoQuery.index) { normalizedMangoQuery.sort = normalizedMangoQuery.index.map((field: string) => { return { [field as any]: 'asc' } as any; }); } else { /** * Find the index that best matches the fields with the logical operators */ if (schema.indexes) { const fieldsWithLogicalOperator: Set = new Set(); Object.entries(normalizedMangoQuery.selector).forEach(([field, matcher]) => { let hasLogical = false; if (typeof matcher === 'object' && matcher !== null) { hasLogical = !!Object.keys(matcher).find(operator => LOGICAL_OPERATORS.has(operator)); } else { hasLogical = true; } if (hasLogical) { fieldsWithLogicalOperator.add(field); } }); let currentFieldsAmount = -1; let currentBestIndexForSort: string[] | readonly string[] | undefined; schema.indexes.forEach(index => { const useIndex = isMaybeReadonlyArray(index) ? index : [index]; const firstWrongIndex = useIndex.findIndex(indexField => !fieldsWithLogicalOperator.has(indexField)); if ( firstWrongIndex > 0 && firstWrongIndex > currentFieldsAmount ) { currentFieldsAmount = firstWrongIndex; currentBestIndexForSort = useIndex; } }); if (currentBestIndexForSort) { normalizedMangoQuery.sort = currentBestIndexForSort.map((field: string) => { return { [field as any]: 'asc' } as any; }); } } /** * Fall back to the primary key as sort order * if no better one has been found */ if (!normalizedMangoQuery.sort) { normalizedMangoQuery.sort = [{ [primaryKey]: 'asc' }] as any; } } } else { const isPrimaryInSort = normalizedMangoQuery.sort .find(p => firstPropertyNameOfObject(p) === primaryKey); if (!isPrimaryInSort) { normalizedMangoQuery.sort = normalizedMangoQuery.sort.slice(0); normalizedMangoQuery.sort.push({ [primaryKey]: 'asc' } as any); } } return normalizedMangoQuery; } /** * @recursive * @mutates the input so that we do not have to deep clone */ export function normalizeQueryRegex( selector: any ): any { if (typeof selector !== 'object' || selector === null) { return selector; } const keys = Object.keys(selector); const ret: any = {}; keys.forEach(key => { const value: any = selector[key]; if ( key === '$regex' && value instanceof RegExp ) { const parsed = parseRegex(value); ret.$regex = parsed.pattern; ret.$options = parsed.flags; } else if (Array.isArray(value)) { ret[key] = value.map(item => normalizeQueryRegex(item)); } else { ret[key] = normalizeQueryRegex(value); } }); return ret; } /** * Returns the sort-comparator, * which is able to sort documents in the same way * a query over the db would do. */ export function getSortComparator( schema: RxJsonSchema> | RxJsonSchema>, query: FilledMangoQuery | FilledMangoQuery> ): DeterministicSortComparator { if (!query.sort) { throw newRxError('SNH', { query }); } const sortParts: { key: string; direction: MangoQuerySortDirection; getValueFn: ObjectPathMonadFunction; }[] = []; query.sort.forEach(sortBlock => { const key = Object.keys(sortBlock)[0]; const direction = Object.values(sortBlock)[0]; sortParts.push({ key, direction, getValueFn: objectPathMonad(key) }); }); const fun: DeterministicSortComparator = (a: RxDocType, b: RxDocType) => { for (let i = 0; i < sortParts.length; ++i) { const sortPart = sortParts[i]; const valueA = sortPart.getValueFn(a); const valueB = sortPart.getValueFn(b); if (valueA !== valueB) { const ret = sortPart.direction === 'asc' ? mingoSortComparator(valueA, valueB) : mingoSortComparator(valueB, valueA); return ret as any; } } }; return fun; } /** * Returns a function * that can be used to check if a document * matches the query. */ export function getQueryMatcher( _schema: RxJsonSchema | RxJsonSchema>, query: FilledMangoQuery | FilledMangoQuery> ): QueryMatcher> { if (!query.sort) { throw newRxError('SNH', { query }); } const mingoQuery = getMingoQuery(query.selector as any); const fun: QueryMatcher> = (doc: RxDocumentData) => { if (doc._deleted) { return false; } const cursor = mingoQuery.find([doc]); const next = cursor.next(); if (next) { return true; } else { return false; } }; return fun; }