UNPKG

lodash-walk-object

Version:

Walk all properties deep in object with lodash help

677 lines 22.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_walk_object_1 = require("./lodash-walk-object"); // adjust path describe('walk.Object', () => { describe('depth-first traversal', () => { it('walks nested objects using lodash paths', () => { const input = { user: { name: 'Darek', address: { city: 'Warsaw', }, }, active: true, }; const visited = []; lodash_walk_object_1.walk.Object(input, (value, path) => { visited.push([path, value]); }, { walkGetters: false, }); expect(visited.map(([path]) => path)).toEqual([ 'user', 'user.name', 'user.address', 'user.address.city', 'active', ]); expect(visited).toContainEqual(['user.name', 'Darek']); expect(visited).toContainEqual(['user.address.city', 'Warsaw']); expect(visited).toContainEqual(['active', true]); }); it('does not invoke the iterator for the root object', () => { const iterator = vi.fn(); lodash_walk_object_1.walk.Object({ first: 1, second: 2, }, iterator, { walkGetters: false, }); expect(iterator).toHaveBeenCalledTimes(2); expect(iterator).not.toHaveBeenCalledWith(expect.anything(), '', expect.anything(), expect.anything()); }); it('walks arrays using bracket notation', () => { const input = { users: [ { name: 'Alice', }, { name: 'Bob', }, ], }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, }); expect(paths).toEqual([ 'users', 'users[0]', 'users[0].name', 'users[1]', 'users[1].name', ]); }); it('walks a root array', () => { const input = [ { id: 1, }, { id: 2, }, ]; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, }); expect(paths).toEqual(['[0]', '[0].id', '[1]', '[1].id']); }); }); describe('breadth-first traversal', () => { it('walks objects level by level', () => { const input = { first: { nested: { value: 1, }, }, second: { value: 2, }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { breadthWalk: true, walkGetters: false, }); expect(paths).toEqual([ 'first', 'second', 'first.nested', 'second.value', 'first.nested.value', ]); }); it('walks array elements level by level', () => { const input = { items: [ { value: 1, }, { value: 2, }, ], footer: 'end', }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { breadthWalk: true, walkGetters: false, }); expect(paths).toEqual([ 'items', 'footer', 'items[0]', 'items[1]', 'items[0].value', 'items[1].value', ]); }); }); describe('changing values', () => { it('changes a primitive object property', () => { const input = { user: { name: 'before', }, }; lodash_walk_object_1.walk.Object(input, (_value, path, changeValueTo) => { if (path === 'user.name') { changeValueTo('after'); } }, { walkGetters: false, }); expect(input.user.name).toBe('after'); }); it('changes an array element', () => { const input = { values: [10, 20, 30], }; lodash_walk_object_1.walk.Object(input, (_value, path, changeValueTo) => { if (path === 'values[1]') { changeValueTo(999); } }, { walkGetters: false, }); expect(input.values).toEqual([10, 999, 30]); }); it('continues walking through a replacement object in depth-first mode', () => { const input = { node: 123, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path, changeValueTo) => { paths.push(path); if (path === 'node') { changeValueTo({ nested: 'created during walk', }); } }, { walkGetters: false, }); expect(input.node).toEqual({ nested: 'created during walk', }); expect(paths).toContain('node.nested'); }); it('can replace an entire nested object', () => { const input = { config: { oldValue: true, }, }; lodash_walk_object_1.walk.Object(input, (_value, path, changeValueTo, options) => { if (path === 'config') { changeValueTo({ newValue: true, }); // Avoid depending on whether replacement children are traversed. options?.skipObject?.(); } }, { walkGetters: false, }); expect(input.config).toEqual({ newValue: true, }); }); }); describe('skipObject()', () => { it('skips children of the current object', () => { const input = { publicData: { value: 1, }, privateData: { password: 'secret', nested: { token: 'token', }, }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path, _changeValueTo, options) => { paths.push(path); if (path === 'privateData') { options?.skipObject?.(); } }, { walkGetters: false, }); expect(paths).toContain('privateData'); expect(paths).not.toContain('privateData.password'); expect(paths).not.toContain('privateData.nested'); expect(paths).not.toContain('privateData.nested.token'); expect(paths).toContain('publicData.value'); }); it('skips only the selected array element contents', () => { const input = { items: [ { id: 1, details: 'first', }, { id: 2, details: 'second', }, ], }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path, _changeValueTo, options) => { paths.push(path); if (path === 'items[0]') { options?.skipObject?.(); } }, { walkGetters: false, }); expect(paths).not.toContain('items[0].id'); expect(paths).not.toContain('items[0].details'); expect(paths).toContain('items[1].id'); expect(paths).toContain('items[1].details'); }); }); describe('exit()', () => { it('stops depth-first traversal', () => { const input = { first: { value: 1, }, stopHere: { value: 2, }, shouldNotBeVisited: { value: 3, }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path, _changeValueTo, options) => { paths.push(path); if (path === 'stopHere') { options?.exit?.(); } }, { walkGetters: false, }); expect(paths).toContain('stopHere'); expect(paths).not.toContain('stopHere.value'); expect(paths).not.toContain('shouldNotBeVisited'); expect(paths).not.toContain('shouldNotBeVisited.value'); }); it('stops breadth-first traversal', () => { const input = { first: { value: 1, }, stopHere: { value: 2, }, third: { value: 3, }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path, _changeValueTo, options) => { paths.push(path); if (path === 'stopHere') { options?.exit?.(); } }, { breadthWalk: true, walkGetters: false, }); expect(paths).toEqual(['first', 'stopHere']); }); }); describe('getters', () => { it('walks getters by default', () => { class Example { regular = 'regular'; get calculated() { return 'getter value'; } } const visited = new Map(); lodash_walk_object_1.walk.Object(new Example(), (value, path, _changeValueTo, options) => { visited.set(path, { value, isGetter: options?.isGetter, }); }); expect(visited.get('regular')).toEqual({ value: 'regular', isGetter: false, }); expect(visited.get('calculated')).toEqual({ value: 'getter value', isGetter: true, }); }); it('does not walk getters when walkGetters is false', () => { const getter = vi.fn(() => 'getter value'); const input = { regular: 'regular', }; Object.defineProperty(input, 'calculated', { enumerable: false, configurable: true, get: getter, }); const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, }); expect(paths).toEqual(['regular']); expect(getter).not.toHaveBeenCalled(); }); it('marks a non-enumerable value as getter-like when walking properties', () => { const input = { visible: true, }; Object.defineProperty(input, 'hidden', { enumerable: false, configurable: true, value: 'hidden value', }); const visited = new Map(); lodash_walk_object_1.walk.Object(input, (_value, path, _changeValueTo, options) => { visited.set(path, options?.isGetter); }); expect(visited.get('visible')).toBe(false); expect(visited.get('hidden')).toBe(true); }); }); describe('include option', () => { it('walks only included branches', () => { const input = { user: { name: 'Darek', address: { city: 'Warsaw', }, }, settings: { theme: 'dark', }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, include: ['user'], }); expect(paths).toEqual([ 'user', 'user.name', 'user.address', 'user.address.city', ]); }); it('supports including a nested branch', () => { const input = { user: { name: 'Darek', address: { city: 'Warsaw', street: 'Example', }, }, settings: { theme: 'dark', }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, include: ['user.address'], }); /* * This expectation documents the desirable behavior: * ancestors required to reach an included nested path should remain * traversable. * * The current _shoudlReturn implementation may fail this test because * "user" does not start with "user.address". */ expect(paths).toEqual([ 'user', 'user.address', 'user.address.city', 'user.address.street', ]); }); }); describe('exclude option', () => { it('excludes an entire branch', () => { const input = { user: { name: 'Darek', password: { hash: 'secret', }, }, settings: { theme: 'dark', }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, exclude: ['user.password'], }); expect(paths).toContain('user'); expect(paths).toContain('user.name'); expect(paths).not.toContain('user.password'); expect(paths).not.toContain('user.password.hash'); expect(paths).toContain('settings.theme'); }); it('excludes multiple branches', () => { const input = { user: { password: 'secret', name: 'Darek', }, metadata: { internal: true, }, visible: { value: true, }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, exclude: ['user.password', 'metadata'], }); expect(paths).not.toContain('user.password'); expect(paths).not.toContain('metadata'); expect(paths).not.toContain('metadata.internal'); expect(paths).toContain('user.name'); expect(paths).toContain('visible.value'); }); }); describe('circular references', () => { it('detects a direct circular reference', () => { const input = { name: 'root', }; input.self = input; const result = lodash_walk_object_1.walk.Object(input, () => { }, { checkCircural: true, walkGetters: false, }); expect(result.circs).toEqual([ { pathToObj: 'self', circuralTargetPath: '', }, ]); }); it('detects a nested circular reference', () => { const parent = { name: 'parent', }; const child = { name: 'child', parent, }; parent.child = child; const result = lodash_walk_object_1.walk.Object(parent, () => { }, { checkCircural: true, walkGetters: false, }); expect(result.circs).toContainEqual({ pathToObj: 'child.parent', circuralTargetPath: '', }); }); it('marks iterator options when the current object is circular', () => { const input = {}; input.nested = input; const circularPaths = []; lodash_walk_object_1.walk.Object(input, (_value, path, _changeValueTo, options) => { if (options?.isCircural) { circularPaths.push(path); } }, { checkCircural: true, walkGetters: false, }); expect(circularPaths).toEqual(['nested']); }); it('does not recurse forever when circular checking is enabled', () => { const input = { value: 1, }; input.self = input; const iterator = vi.fn(); expect(() => { lodash_walk_object_1.walk.Object(input, iterator, { checkCircural: true, walkGetters: false, }); }).not.toThrow(); expect(iterator.mock.calls.length).toBeLessThan(10); }); }); describe('special values', () => { it('visits null without trying to traverse its children', () => { const input = { nullValue: null, nextValue: true, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, }); expect(paths).toEqual(['nullValue', 'nextValue']); }); it('visits undefined values', () => { const input = { missing: undefined, }; const visited = []; lodash_walk_object_1.walk.Object(input, (value, path) => { visited.push([path, value]); }, { walkGetters: false, }); expect(visited).toEqual([['missing', undefined]]); }); it('visits functions as values', () => { const handler = () => 'result'; const input = { handler, }; const visited = []; lodash_walk_object_1.walk.Object(input, (value, path) => { visited.push([path, value]); }, { walkGetters: false, }); expect(visited).toEqual([['handler', handler]]); }); it('walks keys containing dashes and dollar signs', () => { const input = { 'some-key': { $value: 123, }, }; const paths = []; lodash_walk_object_1.walk.Object(input, (_value, path) => { paths.push(path); }, { walkGetters: false, }); expect(paths).toEqual(['some-key', 'some-key.$value']); }); }); }); describe('walk.ObjectBy', () => { it('walks an object stored under a selected property', () => { const context = { metadata: 'untouched', data: { user: { name: 'Darek', }, }, }; const paths = []; lodash_walk_object_1.walk.ObjectBy('data', context, (_value, path) => { paths.push(path); }, { walkGetters: false, }); expect(paths).toEqual(['', 'user', 'user.name']); }); it('allows replacing the selected root property', () => { const context = { data: { old: true, }, }; lodash_walk_object_1.walk.ObjectBy('data', context, (_value, path, changeValueTo, options) => { if (path === '') { changeValueTo({ replacement: true, }); options?.skipObject?.(); } }, { walkGetters: false, }); expect(context.data).toEqual({ replacement: true, }); }); it('does not modify unrelated context properties', () => { const context = { id: 123, data: { value: 'before', }, }; lodash_walk_object_1.walk.ObjectBy('data', context, (_value, path, changeValueTo) => { if (path === 'value') { changeValueTo('after'); } }, { walkGetters: false, }); expect(context).toEqual({ id: 123, data: { value: 'after', }, }); }); }); //# sourceMappingURL=lodash-walk-object.test.js.map