repo_description: A fast, local first, reactive Database for JavaScript Applications https://rxdb.info/
repo_topics: angular browser-database couchdb crdt database firebase graphql indexeddb ionic local-first localstorage nodejs nosql postgres pouchdb react-native realtime realtime-database rxdb sqlite
code: import { LOGICAL_OPERATORS, getQueryPlan } from './query-planner.ts';
  import { getPrimaryFieldOfPrimaryKey } from './rx-schema-helper.ts';
  import type {
      DeepReadonly,
      DeterministicSortComparator,
      FilledMangoQuery,
      MangoQuery,
      MangoQuerySortDirection,
      PreparedQuery,
      QueryMatcher,
      RxDocument,
      RxDocumentData,
      RxJsonSchema,
      RxQuery
  } from './types/index.d.ts';
  import {
      clone,
      firstPropertyNameOfObject,
      toArray,
      isMaybeReadonlyArray,
      objectPathMonad,
      ObjectPathMonadFunction
  } from './plugins/utils/index.ts';
  import {
      compare as mingoSortComparator
  } from 'mingo/util';
  import { newRxError } from './rx-error.ts';
  import { getMingoQuery } from './rx-query-mingo.ts';
  
  /**
   * 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<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>,
      mangoQuery: MangoQuery<RxDocType>,
      /**
       * @performance
       * Count queries do not need sort order.
       * Skipping the sort computation avoids iterating over schema indexes
       * and comparing fields, which improves count query performance.
       */
      skipSort?: boolean
  ): FilledMangoQuery<RxDocType> {
      const primaryKey: string = getPrimaryFieldOfPrimaryKey(schema.primaryKey);
  
      const normalizedMangoQuery: FilledMangoQuery<RxDocType> = 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.
           *
           * This works recursively for nested queries that
           * contain multiple selectors via $and, $or, $nor or $not.
           */
          normalizeQuerySelectorShorthands(normalizedMangoQuery.selector);
      }
  
      /**
       * 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 (skipSort && !normalizedMangoQuery.sort) {
          /**
           * @performance
           * Count queries do not need sorted results.
           * Use a simple primary key sort to avoid the expensive
           * index-matching sort computation.
           */
          normalizedMangoQuery.sort = [{ [primaryKey]: 'asc' }] as any;
      } else 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<string> = new Set();
                  Object.entries(normalizedMangoQuery.selector).forEach(([field, matcher]) => {
                      let hasLogical;
                      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));
                      const matchingFieldCount = firstWrongIndex === -1 ? useIndex.length : firstWrongIndex;
                      if (
                          matchingFieldCount > 0 &&
                          matchingFieldCount > currentFieldsAmount
                      ) {
                          currentFieldsAmount = matchingFieldCount;
                          currentBestIndexForSort = useIndex;
                      }
                  });
                  if (currentBestIndexForSort) {
                      normalizedMangoQuery.sort = currentBestIndexForSort.map((field: string) => {
                          return { [field as any]: 'asc' } as any;
                      });
                  }
  
              }
  
              /**
               * If no good index was found as default sort-order,
               * just use the first index of the schema.
               * If no index is in the schema, use the default-index which
               * is created by RxDB ONLY if there is no other index defined.
               */
              if (!normalizedMangoQuery.sort) {
                  if (schema.indexes && schema.indexes.length > 0) {
                      const firstIndex = schema.indexes[0];
                      const useIndex = isMaybeReadonlyArray(firstIndex) ? firstIndex : [firstIndex];
                      normalizedMangoQuery.sort = useIndex.map(field => ({ [field]: 'asc' })) as any;
                  } else {
                      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;
  }
  
  /**
   * Returns the sort-comparator,
   * which is able to sort documents in the same way
   * a query over the db would do.
   */
  export function getSortComparator<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>,
      query: FilledMangoQuery<RxDocType>
  ): DeterministicSortComparator<RxDocType> {
      if (!query.sort) {
          throw newRxError('SNH', { query });
      }
      const sortParts: {
          key: string;
          direction: MangoQuerySortDirection;
          getValueFn: ObjectPathMonadFunction<RxDocType>;
      }[] = [];
      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<RxDocType> = (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) {
                  /**
                   * @performance
                   * Use a fast inline comparison for common types (string, number)
                   * instead of the more general mingoSortComparator.
                   * Mingo's compare does extra type checks that are unnecessary
                   * when both values share the same primitive type.
                   */
                  const dirMultiplier = sortPart.direction === 'asc' ? 1 : -1;
                  const typeA = typeof valueA;
                  if (typeA === typeof valueB && (typeA === 'string' || typeA === 'number')) {
                      return ((valueA < valueB ? -1 : 1) * dirMultiplier) as any;
                  }
                  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<RxDocType>(
      _schema: RxJsonSchema<RxDocType> | RxJsonSchema<RxDocumentData<RxDocType>>,
      query: FilledMangoQuery<RxDocType>
  ): QueryMatcher<RxDocumentData<RxDocType>> {
      if (!query.sort) {
          throw newRxError('SNH', { query });
      }
  
      const mingoQuery = getMingoQuery(query.selector as any);
      const fun: QueryMatcher<RxDocumentData<RxDocType>> = (doc: RxDocumentData<RxDocType> | DeepReadonly<RxDocumentData<RxDocType>>) => {
          return mingoQuery.test(doc);
      };
      return fun;
  }
  
  
  export async function runQueryUpdateFunction<RxDocType, RxQueryResult>(
      rxQuery: RxQuery<RxDocType, RxQueryResult>,
      fn: (doc: RxDocument<RxDocType>) => Promise<RxDocument<RxDocType>>
  ): Promise<RxQueryResult> {
      const docs = await rxQuery.exec();
      if (!docs) {
          // only findOne() queries can return null
          return null as any;
      }
      if (Array.isArray(docs)) {
          return Promise.all(
              docs.map(doc => fn(doc))
          ) as any;
      } else if (docs instanceof Map) {
          const resultMap = new Map();
          await Promise.all(
              [...docs.entries()].map(async ([key, doc]) => {
                  const updatedDoc = await fn(doc);
                  resultMap.set(key, updatedDoc);
              })
          );
          return resultMap as any;
      } else {
          // via findOne()
          const result = await fn(docs as any);
          return result as any;
      }
  }
  
  /**
   * Normalizes selector shorthand values recursively.
   * Converts `{field: value}` to `{field: {$eq: value}}`
   * and recurses into $and, $or, $nor arrays and $not objects.
   */
  const SELECTOR_ARRAY_OPERATORS = new Set(['$and', '$or', '$nor']);
  function normalizeQuerySelectorShorthands(selector: any): void {
      Object
          .entries(selector)
          .forEach(([field, matcher]) => {
              if (typeof matcher !== 'object' || matcher === null) {
                  selector[field] = { $eq: matcher };
              } else if (SELECTOR_ARRAY_OPERATORS.has(field) && Array.isArray(matcher)) {
                  (matcher as any[]).forEach(subSelector => normalizeQuerySelectorShorthands(subSelector));
              } else if (field === '$not' && typeof matcher === 'object') {
                  normalizeQuerySelectorShorthands(matcher);
              } else if (!field.startsWith('$') && typeof matcher === 'object') {
                  /**
                   * Recurse into field-level operator objects to normalize
                   * sub-selectors like $elemMatch which contain nested selectors.
                   */
                  const matcherObj = matcher as any;
                  if (matcherObj.$elemMatch && typeof matcherObj.$elemMatch === 'object') {
                      normalizeQuerySelectorShorthands(matcherObj.$elemMatch);
                  }
              }
          });
  }
  
  /**
   * @returns a format of the query that can be used with the storage
   * when calling RxStorageInstance().query()
   */
  export function prepareQuery<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>,
      mutateableQuery: FilledMangoQuery<RxDocType>
  ): PreparedQuery<RxDocType> {
      if (!mutateableQuery.sort) {
          throw newRxError('SNH', {
              query: mutateableQuery
          });
      }
  
      /**
       * Store the query plan together with the
       * prepared query to save performance.
       */
      const queryPlan = getQueryPlan(
          schema,
          mutateableQuery
      );
  
      return {
          query: mutateableQuery,
          queryPlan
      };
  }
  