packages/db/src/Storage/Operators/Update/Utils.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import * as dot from "dot-prop";
import { deepEqual } from "fast-equals";
import { Query } from "mingo";

import { clone } from "../../../Clone.js";
import { Document, Filter, WithId } from "../../../Types.js";

type UpdateValue = (data: any, key: string, target: string) => any;

export function setPositionalData<TSchema extends Document = Document>(
  document: WithId<TSchema>,
  criteria: Filter<WithId<TSchema>>,
  key: string,
  update: {
    object: UpdateValue;
    value: UpdateValue;
  }
): boolean {
  const { filter, path, target } = getPositionalFilter(criteria, key);

  const values = getPropertyValues(document, path);
  const items =
    typeof filter === "object"
      ? getPositionalUpdateQuery(clone(values), key, filter, target, update.object)
      : getPositionalUpdate(clone(values), key, filter, target, update.value);

  dot.setProperty(document, path, items);

  return deepEqual(values, items) === false;
}

function getPropertyValues(document: Document, path: string): string[] {
  const values = dot.getProperty(document, path);
  if (values === undefined) {
    throw new Error("Values is undefined");
  }
  if (Array.isArray(values) === false) {
    throw new Error("Values is not an array");
  }
  return values;
}

export function getPositionalUpdate(
  items: any[],
  key: string,
  filter: string,
  target: string,
  updateValue: UpdateValue
): any[] {
  let index = 0;
  for (const item of items) {
    if (item === filter) {
      items[index] = updateValue(items[index], key, target);
      break;
    }
    index += 1;
  }
  return items;
}

export function getPositionalUpdateQuery(
  items: any[],
  key: string,
  filter: Filter<any>,
  target: string,
  updateValue: UpdateValue
): any[] {
  let index = 0;
  for (const item of items) {
    if (new Query(filter).test(item) === true) {
      if (target === "") {
        items[index] = updateValue(items[index], key, target);
      } else {
        dot.setProperty(item, target, updateValue(items[index], key, target));
      }
      break;
    }
    index += 1;
  }
  return items;
}

export function getPositionalFilter(criteria: Filter<any>, key: string): PositionalFilter {
  const [leftPath, rightPath] = key.split("$");

  const lKey = trimSeparators(leftPath);
  const rKey = trimSeparators(rightPath);

  for (const key in criteria) {
    const result = getPositionalCriteriaFilter(key, lKey, rKey, criteria);
    if (result !== undefined) {
      return result;
    }
  }

  return {
    filter: criteria[lKey],
    path: lKey,
    target: rKey
  };
}

function getPositionalCriteriaFilter(
  key: string,
  lKey: string,
  rKey: string,
  criteria: Filter<any>
): PositionalFilter | undefined {
  if (key.includes(lKey) === true) {
    const isObject = typeof criteria[key] === "object";
    if (key.includes(".") === true || isObject === true) {
      return {
        filter:
          trimSeparators(key.replace(lKey, "")) === ""
            ? (criteria[key] as any).$elemMatch !== undefined
              ? (criteria[key] as any).$elemMatch
              : criteria[key]
            : {
                [trimSeparators(key.replace(lKey, ""))]: criteria[key]
              },
        path: lKey,
        target: rKey
      };
    }
  }
  return undefined;
}

function trimSeparators(value: string): string {
  return value.replace(/^\.+|\.+$/gm, "");
}

/**
 * A position filter is used to find documents to update in an array of values.
 *
 * @example
 *
 * ```ts
 * const document = {
 *   grades: [
 *     { grade: 80, mean: 75, std: 8 },
 *     { grade: 85, mean: 90, std: 5 },
 *     { grade: 85, mean: 85, std: 8 }
 *   ]
 * }
 *
 * updateOne({ "grades.grade": 85 }, { $set: { "grades.$.std": 6 } } })
 * ```
 *
 * In the above example the filter would be `{ grade: 85 }` which is used to find
 * objects to update in an array of values.
 */
type PositionalFilter = {
  /**
   * The filter to use to find the values to update in an array.
   */
  filter: any;

  /**
   * The path to the array of values of the parent document. Eg. `grades`.
   */
  path: string;

  /**
   * The path to the key to update in the array of values. Eg. `std`.
   */
  target: string;
};