Skip to content

Commit

Permalink
util(resolveGraph): add preserveIndex option to maintain positions fo…
Browse files Browse the repository at this point in the history
…r arrays in resolved object
  • Loading branch information
kofrasa committed Dec 7, 2024
1 parent f716494 commit b2e2cc1
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 42 deletions.
79 changes: 42 additions & 37 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number> = {
undefined: 1,
Expand Down Expand Up @@ -582,13 +575,26 @@ 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;
while (depth-- && arr.length === 1) arr = arr[0] as 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
Expand All @@ -598,7 +604,7 @@ function unwrap(arr: Any[], depth: number): Any[] {
export function resolve(
obj: ArrayOrObject,
selector: string,
options?: ResolveOptions
options?: Pick<ResolveOptions, "unwrapArray">
): Any {
let depth = 0;
function resolve2(o: ArrayOrObject, path: string[]): Any {
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down
29 changes: 24 additions & 5 deletions test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down

0 comments on commit b2e2cc1

Please sign in to comment.