diff --git a/CHANGELOG.md b/CHANGELOG.md index 20482c2b..6e69f701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 6.6.0 / 2024-11-30 +## 6.5.1 / 2024-12-06 **Improvements** diff --git a/src/index.ts b/src/index.ts index 3a71d34b..b881b4d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,23 @@ import { Cursor } from "./cursor"; import { Source } from "./lazy"; import { Query } from "./query"; import { AnyObject } from "./types"; -import { createUpdater, update } from "./updater"; +import { createUpdater } from "./updater"; export { Aggregator } from "./aggregator"; export { Query } from "./query"; -export { createUpdater, update } from "./updater"; +export { createUpdater } from "./updater"; + +/** + * Updates the given object with the expression. + * + * @param obj The object to update. + * @param expr The update expressions. + * @param arrayFilters Filters to apply to nested items. + * @param conditions Conditions to validate before performing update. + * @param options Update options to override defaults. + * @returns {string[]} A list of modified field paths in the object. + */ +export const update = createUpdater(); /** * Performs a query on a collection and returns a cursor object. diff --git a/src/operators/_predicates.ts b/src/operators/_predicates.ts index 98ea0d04..6774d681 100644 --- a/src/operators/_predicates.ts +++ b/src/operators/_predicates.ts @@ -21,7 +21,6 @@ import { compare as mingoCmp, ensureArray, flatten, - inArray, intersection, isArray, isBoolean, @@ -252,7 +251,7 @@ export function $all( for (const query of queries) { // no need to check all the queries. if (!matched) break; - if (isObject(query) && inArray(Object.keys(query), "$elemMatch")) { + if (isObject(query) && Object.keys(query).includes("$elemMatch")) { matched = $elemMatch(values, query["$elemMatch"] as AnyObject, options); } else if (isRegExp(query)) { matched = values.some(s => typeof s === "string" && query.test(s)); diff --git a/src/operators/expression/type/_internal.ts b/src/operators/expression/type/_internal.ts index 06bde373..497f3483 100644 --- a/src/operators/expression/type/_internal.ts +++ b/src/operators/expression/type/_internal.ts @@ -2,6 +2,11 @@ import { computeValue, Options } from "../../../core"; import { Any, AnyObject } from "../../../types"; import { isDate, isNil, isNumber, isString } from "../../../util"; +export const MAX_INT = 2147483647; +export const MIN_INT = -2147483648; +export const MAX_LONG = Number.MAX_SAFE_INTEGER; +export const MIN_LONG = Number.MIN_SAFE_INTEGER; + export class TypeConvertError extends Error { constructor(message: string) { super(message); @@ -12,9 +17,8 @@ export function toInteger( obj: AnyObject, expr: Any, options: Options, - max: number, min: number, - typename: string + max: number ): number | null { const val = computeValue(obj, expr, null, options) as | string @@ -37,5 +41,7 @@ export function toInteger( } } - throw new TypeConvertError(`cannot convert '${val}' to ${typename}`); + throw new TypeConvertError( + `cannot convert '${val}' to ${max == MAX_INT ? "int" : "long"}` + ); } diff --git a/src/operators/expression/type/toInt.ts b/src/operators/expression/type/toInt.ts index f89016d6..d888118b 100644 --- a/src/operators/expression/type/toInt.ts +++ b/src/operators/expression/type/toInt.ts @@ -4,8 +4,7 @@ import { ExpressionOperator, Options } from "../../../core"; import { Any, AnyObject } from "../../../types"; -import { MAX_INT, MIN_INT } from "../../../util"; -import { toInteger } from "./_internal"; +import { MAX_INT, MIN_INT, toInteger } from "./_internal"; /** * Converts a value to an integer. If the value cannot be converted to an integer, $toInt errors. If the value is null or missing, $toInt returns null. @@ -16,6 +15,4 @@ export const $toInt: ExpressionOperator = ( obj: AnyObject, expr: Any, options: Options -): number | null => { - return toInteger(obj, expr, options, MAX_INT, MIN_INT, "int"); -}; +): number | null => toInteger(obj, expr, options, MIN_INT, MAX_INT); diff --git a/src/operators/expression/type/toLong.ts b/src/operators/expression/type/toLong.ts index 74d0b5e3..987bcd55 100644 --- a/src/operators/expression/type/toLong.ts +++ b/src/operators/expression/type/toLong.ts @@ -4,8 +4,7 @@ import { ExpressionOperator, Options } from "../../../core"; import { Any, AnyObject } from "../../../types"; -import { MAX_LONG, MIN_LONG } from "../../../util"; -import { toInteger } from "./_internal"; +import { MAX_LONG, MIN_LONG, toInteger } from "./_internal"; /** * Converts a value to a long. If the value cannot be converted to a long, $toLong errors. If the value is null or missing, $toLong returns null. @@ -16,6 +15,4 @@ export const $toLong: ExpressionOperator = ( obj: AnyObject, expr: Any, options: Options -): number | null => { - return toInteger(obj, expr, options, MAX_LONG, MIN_LONG, "long"); -}; +): number | null => toInteger(obj, expr, options, MIN_LONG, MAX_LONG); diff --git a/src/operators/expression/type/type.ts b/src/operators/expression/type/type.ts index 4e3273d4..eda03a51 100644 --- a/src/operators/expression/type/type.ts +++ b/src/operators/expression/type/type.ts @@ -4,7 +4,8 @@ import { computeValue, ExpressionOperator, Options } from "../../../core"; import { Any, AnyObject } from "../../../types"; -import { isNumber, isRegExp, MAX_INT, MIN_INT, typeOf } from "../../../util"; +import { isNumber, isRegExp, typeOf } from "../../../util"; +import { MAX_INT, MIN_INT } from "./_internal"; export const $type: ExpressionOperator = ( obj: AnyObject, diff --git a/src/operators/pipeline/project.ts b/src/operators/pipeline/project.ts index 16ec7e9f..2681c9ab 100644 --- a/src/operators/pipeline/project.ts +++ b/src/operators/pipeline/project.ts @@ -13,23 +13,22 @@ import { ensureArray, filterMissing, has, - inArray, into, isArray, isEmpty, - isMissing, isNil, isNumber, isObject, isOperator, - isPrimitive, isString, - notInArray, + merge, removeValue, resolveGraph, setValue } from "../../util"; +const DESCRIPTORS = new Set(Array.from([0, 1, false, true])); + /** * Reshapes each document in the stream, such as by adding new fields or removing existing fields. For each input document, outputs one document. * @@ -49,18 +48,14 @@ export const $project: PipelineOperator = ( // result collection const expressionKeys = Object.keys(expr); - let idOnlyExcluded = false; // validate inclusion and exclusion validateExpression(expr, options); const ID_KEY = options.idKey; - if (inArray(expressionKeys, ID_KEY)) { - const id = expr[ID_KEY]; - idOnlyExcluded = id === 0 && expressionKeys.length === 1; - } else { - // if not specified the add the ID field + if (!expressionKeys.includes(ID_KEY)) { + // if not specified then add the ID field expressionKeys.push(ID_KEY); } @@ -102,7 +97,7 @@ function processObject( // expression to associate with key const subExpr = expr[key]; - if (key !== options.idKey && inArray([0, false], subExpr)) { + if (key !== options.idKey && (subExpr === 0 || subExpr === false)) { foundExclusion = true; } @@ -111,7 +106,7 @@ function processObject( value = obj[key]; } else if (isString(subExpr)) { value = computeValue(obj, subExpr, key, options); - } else if (inArray([1, true], subExpr)) { + } else if (subExpr === 1 || subExpr === true) { // For direct projections, we use the resolved object value } else if (isArray(subExpr)) { value = subExpr.map(v => { @@ -185,7 +180,7 @@ function processObject( } // if computed add/or remove accordingly - if (notInArray([0, 1, false, true], subExpr)) { + if (!DESCRIPTORS.has(subExpr)) { if (value === undefined) { removeValue(newObj, key, { descendArray: true }); } else { @@ -234,28 +229,3 @@ function validateExpression(expr: AnyObject, options: Options): void { ); } } - -/** - * Deep merge objects or arrays. When the inputs have unmergeable types, the right hand value is returned. - * If inputs are arrays and options.flatten is set, elements in the same position are merged together. - * Remaining elements are appended to the target object. - * - * @param target Target object to merge into. - * @param input Source object to merge from. - */ -function merge(target: Any, input: Any): Any { - // take care of missing inputs - if (isMissing(target) || isNil(target)) return input; - if (isMissing(input) || isNil(input)) return target; - if (isPrimitive(target) || isPrimitive(input)) return input; - if (isArray(target) && isArray(input)) { - assert( - target.length === input.length, - "arrays must be of equal length to merge." - ); - } - for (const k in input as AnyObject) { - target[k] = merge(target[k], input[k]); - } - return target; -} diff --git a/src/operators/pipeline/sort.ts b/src/operators/pipeline/sort.ts index c6c28484..fe82e59b 100644 --- a/src/operators/pipeline/sort.ts +++ b/src/operators/pipeline/sort.ts @@ -2,9 +2,9 @@ import { CollationSpec, Options, PipelineOperator } from "../../core"; import { Iterator } from "../../lazy"; import { Any, AnyObject, Comparator } from "../../types"; import { + assert, compare, groupBy, - into, isEmpty, isObject, isString, @@ -48,12 +48,10 @@ export const $sort: PipelineOperator = ( const sortedKeys = Array.from(groups.keys()).sort(cmp); if (sortKeys[key] === -1) sortedKeys.reverse(); - // reuse collection so the data is available for the next iteration of the sort modifiers. - coll = []; - sortedKeys.reduce( - (acc: Any[], key: Any) => into(acc, groups.get(key)), - coll - ); + // modify collection in place. + let i = 0; + for (const k of sortedKeys) for (const v of groups.get(k)) coll[i++] = v; + assert(i == coll.length, "bug: counter must match collection size."); } return coll; }); diff --git a/src/query.ts b/src/query.ts index 395f55aa..e3491dce 100644 --- a/src/query.ts +++ b/src/query.ts @@ -2,14 +2,11 @@ import { getOperator, initOptions, Options, QueryOperator } from "./core"; import { Cursor } from "./cursor"; import { Source } from "./lazy"; import { Any, AnyObject, Callback, Predicate } from "./types"; -import { - assert, - inArray, - isObject, - isOperator, - MingoError, - normalize -} from "./util"; +import { assert, isObject, isOperator, MingoError, normalize } from "./util"; + +const TOP_LEVEL_OPS = new Set( + Array.from(["$and", "$or", "$nor", "$expr", "$jsonSchema"]) +); /** * An object used to filter input documents @@ -41,9 +38,7 @@ export class Query { for (const [field, expr] of Object.entries(this.#condition)) { if ("$where" === field) { Object.assign(whereOperator, { field: field, expr: expr }); - } else if ( - inArray(["$and", "$or", "$nor", "$expr", "$jsonSchema"], field) - ) { + } else if (TOP_LEVEL_OPS.has(field)) { this.processOperator(field, field, expr); } else { // normalize expression diff --git a/src/updater.ts b/src/updater.ts index 025b453c..6edb8122 100644 --- a/src/updater.ts +++ b/src/updater.ts @@ -86,17 +86,6 @@ export function createUpdater(defaultOptions?: UpdateOptions): Updater { } /** - * Updates the given object with the expression. - * - * @param obj The object to update. - * @param expr The update expressions. - * @param arrayFilters Filters to apply to nested items. - * @param conditions Conditions to validate before performing update. - * @param options Update options to override defaults. - * @returns {string[]} A list of modified field paths in the object. + * @deprecated Use {@link update}. */ -export const update = createUpdater(); -/** - * @deprecated Alias to {@link update} - */ -export const updateObject = update; +export const updateObject = createUpdater(); diff --git a/src/util.ts b/src/util.ts index f7681535..d87172eb 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,25 +1,18 @@ /** * Utility constants and functions */ - import { Any, AnyObject, ArrayOrObject, Callback, Comparator, - GroupByOutput, HashFunction } from "./types"; /** Represents an error reported by the mingo library. */ export class MingoError extends Error {} -export const MAX_INT = 2147483647; -export const MIN_INT = -2147483648; -export const MAX_LONG = Number.MAX_SAFE_INTEGER; -export const MIN_LONG = Number.MIN_SAFE_INTEGER; - // special value to identify missing items. treated differently from undefined const MISSING = Symbol("missing"); @@ -27,8 +20,6 @@ const CYCLE_FOUND_ERROR = Object.freeze( new Error("mingo: cycle detected while processing object/array") ) as Error; -const OBJECT_TAG = "[object Object]"; - type Constructor = new (...args: Any[]) => Any; /** @@ -38,18 +29,23 @@ type Constructor = new (...args: Any[]) => Any; * @returns {number} */ const DEFAULT_HASH_FUNCTION: HashFunction = (value: Any): number => { - const s = toString(value, new Set()); + const s = stringify(value); let hash = 0; let i = s.length; while (i) hash = ((hash << 5) - hash) ^ s.charCodeAt(--i); return hash >>> 0; }; -export const EMPTY_ARRAY = [] as const; +const objectProto = Object.prototype; +const arrayProto = Array.prototype; +const getPrototypeOf = Object.getPrototypeOf; -export const isPrimitive = (v: Any): boolean => +const isPrimitive = (v: Any): boolean => (typeof v !== "object" && typeof v !== "function") || v === null; +/** Scalar types provided by the JS runtime. Includes primitives, RegExp, and Date */ +const isScalar = (v: Any) => isPrimitive(v) || isDate(v) || isRegExp(v); + /** Options to resolve() and resolveGraph() functions */ interface ResolveOptions { unwrapArray?: boolean; @@ -57,29 +53,20 @@ interface ResolveOptions { preserveKeys?: boolean; } -// no array, object, or function types -const JS_SIMPLE_TYPES = new Set([ - "null", - "undefined", - "boolean", - "number", - "string", - "date", - "regex" -]); - /** MongoDB sort comparison order. https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order */ -const SORT_ORDER_BY_TYPE: Record = { - null: 0, - undefined: 0, - number: 1, - string: 2, - object: 3, - array: 4, - boolean: 5, - date: 6, - regex: 7, - function: 8 +const SORT_ORDER: Record = { + undefined: 1, + null: 2, + number: 3, + string: 4, + symbol: 5, + object: 6, + array: 7, + arraybuffer: 8, + boolean: 9, + date: 10, + regexp: 11, + function: 12 }; /** @@ -92,18 +79,12 @@ const SORT_ORDER_BY_TYPE: Record = { export const compare = (a: T, b: T): number => { if (a === MISSING) a = undefined; if (b === MISSING) b = undefined; - const [u, v] = [a, b].map(n => SORT_ORDER_BY_TYPE[typeOf(n)]); + const [u, v] = [a, b].map(n => SORT_ORDER[typeOf(n)]); if (u !== v) return u - v; - // number | string | date - if (u === 1 || u === 2 || u === 6) { - if ((a as number) < (b as number)) return -1; - if ((a as number) > (b as number)) return 1; - return 0; - } // check for equivalence equality if (isEqual(a, b)) return 0; - if ((a as number) < (b as number)) return -1; - if ((a as number) > (b as number)) return 1; + if ((a as string) < (b as string)) return -1; + if ((a as string) > (b as string)) return 1; // if we get here we are comparing a type that does not make sense. return 0; }; @@ -122,10 +103,7 @@ export class ValueMap extends Map { // returns a tuple of [, ]. Expects an object key. #unpack = (key: K): [K, number] => { const hash = this.#hashFn(key); - return [ - (this.#keyMap.get(hash) || EMPTY_ARRAY).find(k => isEqual(k, key)), - hash - ]; + return [(this.#keyMap.get(hash) || []).find(k => isEqual(k, key)), hash]; }; private constructor() { @@ -220,17 +198,15 @@ export function assert(condition: boolean, message: string): void { } /** - * Returns the name of type. - * @param v A value + * Returns the name of type in lowercase. + * @param v Any value */ export const typeOf = (v: Any): string => { - if (v === null) return "null"; - const n = typeof v; - if (n !== "object") return n; - if (isDate(v)) return "date"; - if (isArray(v)) return "array"; - if (isRegExp(v)) return "regex"; - return n; + const s = objectProto.toString.call(v) as string; + const t = s.substring(8, s.length - 1).toLowerCase(); + if (t !== "object") return t; + const ctor = v.constructor; + return ctor == null || ctor === Object ? t : ctor.name; }; export const isBoolean = (v: Any): v is boolean => typeof v === "boolean"; export const isString = (v: Any): v is string => typeof v === "string"; @@ -242,11 +218,8 @@ export const isNotNaN = (v: Any) => export const isArray = Array.isArray; export const isObject = (v: Any): v is object => { if (!v) return false; - const proto = Object.getPrototypeOf(v) as Any; - return ( - (proto === Object.prototype || proto === null) && - OBJECT_TAG === Object.prototype.toString.call(v) - ); + const p = Object.getPrototypeOf(v) as Any; + return (p === Object.prototype || p === null) && typeOf(v) === "object"; }; // objects, arrays, functions, date, custom object export const isObjectLike = (v: Any): boolean => !isPrimitive(v); @@ -254,9 +227,6 @@ export const isDate = (v: Any): v is Date => v instanceof Date; export const isRegExp = (v: Any): v is RegExp => v instanceof RegExp; export const isFunction = (v: Any): boolean => typeof v === "function"; export const isNil = (v: Any): boolean => v === null || v === undefined; -export const inArray = (arr: Any[], item: Any): boolean => arr.includes(item); -export const notInArray = (arr: Any[], item: Any): boolean => - !inArray(arr, item); export const truthy = (arg: Any, strict = true): boolean => !!arg || (strict && arg === ""); export const isEmpty = (x: Any): boolean => @@ -264,34 +234,40 @@ export const isEmpty = (x: Any): boolean => (isString(x) && !x) || (isArray(x) && x.length === 0) || (isObject(x) && Object.keys(x).length === 0); - -export const isMissing = (v: Any): boolean => v === MISSING; /** ensure a value is an array or wrapped within one. */ export const ensureArray = (x: T | T[]): T[] => (isArray(x) ? x : [x]); export const has = (obj: object, prop: string): boolean => - !!obj && (Object.prototype.hasOwnProperty.call(obj, prop) as boolean); + !!obj && (objectProto.hasOwnProperty.call(obj, prop) as boolean); const isTypedArray = (v: Any): boolean => typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(v); -const cloneInternal = (v: Any, refs: Set): Any => { - if (refs.has(v)) throw CYCLE_FOUND_ERROR; - if (isPrimitive(v)) return v; - if (isDate(v)) return new Date(v); - if (isRegExp(v)) return new RegExp(v); +/** + * Deep clone an object. + */ +export const cloneDeep = (v: T, refs?: Set): T => { + // if (structuredClone) return structuredClone(v); + if (isNil(v) || isBoolean(v) || isNumber(v) || isString(v)) return v; + if (isDate(v)) return new Date(v) as T; + if (isRegExp(v)) return new RegExp(v) as T; if (isTypedArray(v)) { const ctor = v.constructor as Constructor; - return new ctor(v); + return new ctor(v) as T; } - + if (!(refs instanceof Set)) refs = new Set(); + if (refs.has(v)) throw CYCLE_FOUND_ERROR; + refs.add(v); try { - refs.add(v); - if (isArray(v)) return v.map(e => cloneInternal(e, refs)) as Any; + if (isArray(v)) { + const arr = new Array(v.length); + for (let i = 0; i < v.length; i++) arr[i] = cloneDeep(v[i], refs); + return arr as T; + } if (isObject(v)) { - const res = {}; - for (const k of Object.keys(v)) res[k] = cloneInternal(v[k], refs); - return res; + const obj: AnyObject = {}; + for (const k of Object.keys(v)) obj[k] = cloneDeep(v[k], refs); + return obj as T; } } finally { refs.delete(v); @@ -301,10 +277,33 @@ const cloneInternal = (v: Any, refs: Set): Any => { return v; }; +const isMissing = (v: Any): boolean => v === MISSING; + /** - * Deep clone an object. Value types and immutable objects are returned as is. + * Deep merge objects or arrays. When the inputs have unmergeable types, the right hand value is returned. + * If inputs are arrays, elements in the same position are merged together. + * Remaining elements are appended to the target object. + * + * @param target Target object to merge into. + * @param input Source object to merge from. + * @private */ -export const cloneDeep = (obj: Any): Any => cloneInternal(obj, new Set()); +export function merge(target: Any, input: Any): Any { + // take care of missing inputs + if (isMissing(target) || isNil(target)) return input; + if (isMissing(input) || isNil(input)) return target; + if (isPrimitive(target) || isPrimitive(input)) return input; + if (isArray(target) && isArray(input)) { + assert( + target.length === input.length, + "arrays must be of equal length to merge." + ); + } + for (const k in input as AnyObject) { + target[k] = merge(target[k], input[k]); + } + return target; +} /** * Returns the intersection of multiple arrays. @@ -357,29 +356,6 @@ export function flatten(xs: Any[], depth = 1): Any[] { return arr; } -/** Returns all members of the value in an object literal. */ -const getMembersOf = (value: Any): [AnyObject, Any] => { - let [proto, names] = [ - Object.getPrototypeOf(value), - Object.getOwnPropertyNames(value) - ] as [Any, string[]]; - // save effective prototype - let activeProto = proto; - // traverse the prototype hierarchy until we get property names or hit the bottom prototype. - while ( - !names.length && - proto !== Object.prototype && - proto !== Array.prototype - ) { - activeProto = proto; - names = Object.getOwnPropertyNames(proto); - proto = Object.getPrototypeOf(proto); - } - const o = {}; - names.forEach(k => (o[k] = (value as AnyObject)[k])); - return [o, activeProto]; -}; - type Stringer = { toString(): string }; /** @@ -418,12 +394,10 @@ export function isEqual(a: Any, b: Any): boolean { return true; } // toString() compare all supported types including custom ones. - const proto = Object.getPrototypeOf(a) as object; + const proto = getPrototypeOf(a) as object; const cmp = isTypedArray(a) || - (proto !== Object.prototype && - proto !== Array.prototype && - (Object.prototype.hasOwnProperty.call(proto, "toString") as boolean)); + (proto !== objectProto && proto !== arrayProto && has(proto, "toString")); return cmp && (a as Stringer).toString() === (b as Stringer).toString(); } @@ -444,53 +418,43 @@ export function unique( /** * Encode value to string using a simple non-colliding stable scheme. * Handles user-defined types by processing keys on first non-empty prototype. - * If a user-defined type provides a "toJSON" function, it is used. + * If a user-defined type provides a "toString" function, it is used. * * @param value The value to convert to a string representation. - * @returns {String} + * @returns {string} */ -export const toString = (v: Any, cycle = new Set()): string => { - const kind = typeOf(v); - switch (kind) { - case "boolean": - case "string": - case "number": - return JSON.stringify(v); - case "date": - return (v as Date).toISOString(); - case "undefined": - case "null": - return kind; - case "symbol": - case "function": - case "regex": - return (v as Stringer).toString(); - } - const ctor = v.constructor; +export const stringify = (v: Any, refs?: Set): string => { + if (v === null) return "null"; + if (v === undefined) return "undefined"; + if (isString(v) || isNumber(v) || isBoolean(v)) return JSON.stringify(v); + if (isDate(v)) return v.toISOString(); + if (isRegExp(v) || isSymbol(v) || isFunction(v)) + return (v as Stringer).toString(); if (isTypedArray(v)) - return ctor.name + "[" + (v as Stringer).toString() + "]"; - if (cycle.has(v)) throw CYCLE_FOUND_ERROR; + return typeOf(v) + "[" + (v as Stringer).toString() + "]"; + if (!(refs instanceof Set)) refs = new Set(); + if (refs.has(v)) throw CYCLE_FOUND_ERROR; try { - cycle.add(v); - if (isArray(v)) return "[" + v.map(s => toString(s, cycle)).join(",") + "]"; + refs.add(v); + if (isArray(v)) return "[" + v.map(s => stringify(s, refs)).join(",") + "]"; if (isObject(v)) { const keys = Object.keys(v).sort(); - return "{" + keys.map(k => `${k}:${toString(v[k], cycle)}`).join() + "}"; + return "{" + keys.map(k => `${k}:${stringify(v[k], refs)}`).join() + "}"; } - // use toString represenation of custom-type + // use toString representation of custom-type const proto = Object.getPrototypeOf(v) as object; if ( - proto !== Object.prototype && - proto !== Array.prototype && - (Object.prototype.hasOwnProperty.call(proto, "toString") as boolean) + proto !== objectProto && + proto !== arrayProto && + has(proto, "toString") ) { - return ctor.name + "(" + JSON.stringify((v as Stringer).toString()) + ")"; + return typeOf(v) + "(" + JSON.stringify((v as Stringer).toString()) + ")"; } throw new Error( "mingo: cannot stringify custom type without explicit toString() method." ); } finally { - cycle.delete(v); + refs.delete(v); } }; @@ -513,13 +477,13 @@ export function hashCode(value: Any, hashFunction?: HashFunction): number { * * @param collection * @param keyFn {Function} to compute the group key of an item in the collection - * @returns {GroupByOutput} + * @returns {Map} */ export function groupBy( collection: Any[], keyFn: Callback, hashFunction: HashFunction = DEFAULT_HASH_FUNCTION -): GroupByOutput { +): Map { if (collection.length < 1) return new Map(); // map of hash to collided values @@ -574,28 +538,26 @@ const MAX_ARRAY_PUSH = 50000; * * @param {*} target The target object * @param {*} rest The array of elements to merge into dest + * @private */ export function into( target: ArrayOrObject, ...rest: ArrayOrObject[] ): ArrayOrObject { if (isArray(target)) { - return rest.reduce( - ((acc, arr: Any[]) => { - // push arrary in batches to handle large inputs - let i = Math.ceil(arr.length / MAX_ARRAY_PUSH); - let begin = 0; - while (i-- > 0) { - Array.prototype.push.apply( - acc, - arr.slice(begin, begin + MAX_ARRAY_PUSH) - ); - begin += MAX_ARRAY_PUSH; - } - return acc; - }) as Callback, - target - ); + for (const arr of rest as Any[][]) { + // push arrary in batches to handle large inputs + let i = Math.ceil(arr.length / MAX_ARRAY_PUSH); + let begin = 0; + while (i-- > 0) { + Array.prototype.push.apply( + target, + arr.slice(begin, begin + MAX_ARRAY_PUSH) + ); + begin += MAX_ARRAY_PUSH; + } + } + return target; } else { // merge objects. same behaviour as Object.assign return rest.filter(isObjectLike).reduce((acc, item) => { @@ -613,7 +575,7 @@ export function into( * @private */ function getValue(obj: ArrayOrObject, key: string | number): Any { - return isObjectLike(obj) ? obj[key] : undefined; + return isArray(obj) || isObject(obj) ? obj[key] : undefined; } /** @@ -639,20 +601,17 @@ export function resolve( options?: ResolveOptions ): Any { let depth = 0; - function resolve2(o: ArrayOrObject, path: string[]): Any { let value: Any = o; for (let i = 0; i < path.length; i++) { const field = path[i]; const isText = /^\d+$/.exec(field) === null; - // using instanceof to aid typescript compiler if (isText && isArray(value)) { // On the first iteration, we check if we received a stop flag. // If so, we stop to prevent iterating over a nested array value // on consecutive object keys in the selector. if (i === 0 && depth > 0) break; - depth += 1; // only look at the rest of the path const subpath = path.slice(i); @@ -670,13 +629,8 @@ export function resolve( return value; } - const result = JS_SIMPLE_TYPES.has(typeOf(obj)) - ? obj - : resolve2(obj, selector.split(".")); - - return isArray(result) && options?.unwrapArray - ? unwrap(result, depth) - : result; + const res = isScalar(obj) ? obj : resolve2(obj, selector.split(".")); + return isArray(res) && options?.unwrapArray ? unwrap(res, depth) : res; } /** @@ -738,6 +692,7 @@ export function resolveGraph( * Filter out all MISSING values from the object in-place * * @param obj The object to filter + * @private */ export function filterMissing(obj: ArrayOrObject): void { if (isArray(obj)) { @@ -889,19 +844,14 @@ export function isOperator(name: string): boolean { * @returns {*} */ export function normalize(expr: Any): Any { - // normalized primitives - if (JS_SIMPLE_TYPES.has(typeOf(expr))) { + if (isScalar(expr)) { return isRegExp(expr) ? { $regex: expr } : { $eq: expr }; } // normalize object expression. using ObjectLike handles custom types if (isObjectLike(expr)) { - const exprObj = expr as AnyObject; // no valid query operator found, so we do simple comparison - if (!Object.keys(exprObj).some(isOperator)) { - return { $eq: expr }; - } - + if (!Object.keys(expr as AnyObject).some(isOperator)) return { $eq: expr }; // ensure valid regex if (has(expr as AnyObject, "$regex")) { const newExpr = { ...(expr as AnyObject) }; diff --git a/test/updater.test.ts b/test/updater.test.ts index 2833f0a5..290fc3cc 100644 --- a/test/updater.test.ts +++ b/test/updater.test.ts @@ -1,5 +1,5 @@ +import { update } from "../src"; import { clone } from "../src/operators/update/_internal"; -import { update } from "../src/updater"; import { isArray } from "../src/util"; describe("updateObject", () => { diff --git a/test/util.test.ts b/test/util.test.ts index 9478fa31..307140fc 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -13,8 +13,9 @@ import { normalize, resolve, resolveGraph, - toString, + stringify, truthy, + typeOf, unique, ValueMap, walk @@ -52,6 +53,34 @@ describe("util", () => { }); }); + class Custom { + constructor(readonly _id: string) {} + toString() { + return this._id; + } + } + + describe("typeOf", () => { + it.each([ + ["null", null], + ["undefined", undefined], + ["number", NaN], + ["number", 1], + ["string", ""], + ["regexp", /a/], + ["boolean", true], + ["boolean", false], + ["symbol", Symbol("a")], + ["error", new Error()], + ["array", []], + ["object", {}], + ["arraybuffer", new ArrayBuffer(0)], + ["Custom", new Custom("abc")] + ])("should expect %p for %p", (res, input) => { + expect(typeOf(input)).toEqual(res); + }); + }); + describe("isEqual", () => { it.each([ [NaN, 0 / 0, true], @@ -88,7 +117,7 @@ describe("util", () => { }); }); - describe("toString", () => { + describe("stringify", () => { const a: Any[] = [1, 2, 3]; const b: Any[] = [4, 5, 6]; @@ -103,15 +132,15 @@ describe("util", () => { [[1, "a"], '[1,"a"]'], [new Date("2001-01-01T00:00:00.000Z"), "2001-01-01T00:00:00.000Z"], [(id: Any) => id, "(id) => id"], - [new Uint8Array([5, 2]), "Uint8Array[5,2]"], - [new Float32Array([1.5, 2.5]), "Float32Array[1.5,2.5]"], + [new Uint8Array([5, 2]), "uint8array[5,2]"], + [new Float32Array([1.5, 2.5]), "float32array[1.5,2.5]"], [{ a: a, b: a }, "{a:[1,2,3],b:[1,2,3]}"], [[a, a], "[[1,2,3],[1,2,3]]"], [[a, b], "[[1,2,3],[4,5,6]]"], [[a, b, a, b], "[[1,2,3],[4,5,6],[1,2,3],[4,5,6]]"], [ObjectId("1234567890"), 'objectId("1234567890")'] ])("should pass: %p => %p", (input, output) => { - expect(toString(input)).toEqual(output); + expect(stringify(input)).toEqual(output); }); it("should check for cycles in object", () => { @@ -120,7 +149,7 @@ describe("util", () => { const obj = { a, b }; b.push(obj); - expect(() => toString(obj)).toThrow(/cycle detected/); + expect(() => stringify(obj)).toThrow(/cycle detected/); }); it("should check for cycles in array", () => { @@ -129,7 +158,7 @@ describe("util", () => { const c = [a, b]; a.push(c); - expect(() => toString(c)).toThrow(/cycle detected/); + expect(() => stringify(c)).toThrow(/cycle detected/); }); });