diff --git a/src/util.ts b/src/util.ts index d87172eb..6d32c60c 100644 --- a/src/util.ts +++ b/src/util.ts @@ -46,13 +46,6 @@ const isPrimitive = (v: Any): boolean => /** 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; - preserveMissing?: boolean; - preserveKeys?: boolean; -} - /** MongoDB sort comparison order. https://www.mongodb.com/docs/manual/reference/bson-type-comparison-order */ const SORT_ORDER: Record = { undefined: 1, @@ -582,6 +575,7 @@ function getValue(obj: ArrayOrObject, key: string | number): Any { * Unwrap a single element array to specified depth * @param {Array} arr * @param {Number} depth + * @private */ function unwrap(arr: Any[], depth: number): Any[] { if (depth < 1) return arr; @@ -589,6 +583,18 @@ function unwrap(arr: Any[], depth: number): Any[] { return arr; } +/** Options to resolve() and resolveGraph() functions */ +interface ResolveOptions { + /** Unwrap the final array value. */ + unwrapArray?: boolean; + /** Replace "undefined" values with special MISSING symbol value. */ + preserveMissing?: boolean; + /** Preserve values for untouched keys of objects. */ + preserveKeys?: boolean; + /** Preserve untouched indexes in arrays. */ + preserveIndex?: boolean; +} + /** * Resolve the value of the field (dot separated) on the given object * @param obj {AnyObject} the object context @@ -598,7 +604,7 @@ function unwrap(arr: Any[], depth: number): Any[] { export function resolve( obj: ArrayOrObject, selector: string, - options?: ResolveOptions + options?: Pick ): Any { let depth = 0; function resolve2(o: ArrayOrObject, path: string[]): Any { @@ -635,7 +641,6 @@ export function resolve( /** * Returns the full object to the resolved value given by the selector. - * This function excludes empty values as they aren't practically useful. * * @param obj {AnyObject} the object context * @param selector {String} dot separated path to field @@ -645,47 +650,47 @@ export function resolveGraph( selector: string, options?: ResolveOptions ): ArrayOrObject | undefined { - const names: string[] = selector.split("."); - const key = names[0]; - // get the next part of the selector - const next = names.slice(1).join("."); - const isIndex = /^\d+$/.test(key); - const hasNext = names.length > 1; - let result: Any; - let value: Any; + const sep = selector.indexOf("."); + const key = sep == -1 ? selector : selector.substring(0, sep); + const next = selector.substring(sep + 1); + const hasNext = sep != -1; if (isArray(obj)) { + // obj is an array + const isIndex = /^\d+$/.test(key); + const arr = isIndex && options?.preserveIndex ? [...obj] : []; if (isIndex) { - result = getValue(obj, Number(key)) as ArrayOrObject; + const index = parseInt(key); + let value = getValue(obj, index) as ArrayOrObject; if (hasNext) { - result = resolveGraph(result as ArrayOrObject, next, options); + value = resolveGraph(value, next, options); + } + if (options?.preserveIndex) { + arr[index] = value; + } else { + arr.push(value); } - result = [result]; } else { - result = []; for (const item of obj) { - value = resolveGraph(item as ArrayOrObject, selector, options); + const value = resolveGraph(item as ArrayOrObject, selector, options); if (options?.preserveMissing) { - if (value === undefined) { - value = MISSING; - } - (result as Any[]).push(value); - } else if (value !== undefined) { - (result as Any[]).push(value); + arr.push(value == undefined ? MISSING : value); + } else if (value != undefined || options?.preserveIndex) { + arr.push(value); } } } - } else { - value = getValue(obj, key); - if (hasNext) { - value = resolveGraph(value as ArrayOrObject, next, options); - } - if (value === undefined) return undefined; - result = options?.preserveKeys ? { ...obj } : {}; - (result as AnyObject)[key] = value; + return arr; } - return result as ArrayOrObject; + const res = options?.preserveKeys ? { ...obj } : {}; + let value = getValue(obj, key); + if (hasNext) { + value = resolveGraph(value as ArrayOrObject, next, options); + } + if (value === undefined) return undefined; + res[key] = value; + return res; } /** diff --git a/test/util.test.ts b/test/util.test.ts index 307140fc..49ac29ec 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -262,31 +262,50 @@ describe("util", () => { it("resolves the path to the selected field only", () => { const result = resolveGraph(doc, "b.e.1"); - expect({ b: { e: [2] } }).toEqual(result); + expect(result).toEqual({ b: { e: [2] } }); expect(doc).toEqual(sameDoc); }); it("resolves item in nested array by index", () => { const result = resolveGraph({ a: [5, { b: [10] }] }, "a.1.b.0"); - expect({ a: [{ b: [10] }] }).toEqual(result); + expect(result).toEqual({ a: [{ b: [10] }] }); }); it("resolves object in a nested array", () => { const result = resolveGraph({ a: [{ b: [{ c: 1 }] }] }, "a.b.c"); - expect({ a: [{ b: [{ c: 1 }] }] }).toEqual(result); + expect(result).toEqual({ a: [{ b: [{ c: 1 }] }] }); }); - it("preserves other keys of the resolved object graph", () => { + it("preserves untouched keys of the resolved object", () => { const result = resolveGraph(doc, "b.e.1", { preserveKeys: true }) as AnyObject; - expect({ a: 1, b: { c: 2, d: ["hello"], e: [2] } }).toEqual(result); + expect(result).toEqual({ a: 1, b: { c: 2, d: ["hello"], e: [2] } }); expect(doc).toEqual(sameDoc); const leaf = resolve(result, "b.d"); expect(leaf).toEqual(["hello"]); expect(leaf === doc.b.d).toBeTruthy(); }); + + it("preserves untouched array indexes of resolved object graph", () => { + const result = resolveGraph(doc, "b.e.1", { + preserveIndex: true + }) as AnyObject; + expect(result).toEqual({ b: { e: [1, 2, 3] } }); + + const res2 = resolveGraph({ a: 1, b: [{ c: 2 }, { d: 3 }] }, "b.1.d", { + preserveIndex: true + }) as AnyObject; + expect(res2).toEqual({ b: [{ c: 2 }, { d: 3 }] }); + }); + + it("preserves position of touched array indexes for nested object in resolved object", () => { + const result = resolveGraph({ a: 1, b: [{ c: 2 }, { d: 3 }] }, "b.d", { + preserveIndex: true + }) as AnyObject; + expect(result).toEqual({ b: [undefined, { d: 3 }] }); + }); }); describe("unique", () => {