UNPKG

lodash-walk-object

Version:

Walk all properties deep in object with lodash help

424 lines 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const lib_1 = require("tnp-core/lib"); const lodash_walk_object_1 = require("./lodash-walk-object"); /** * Simulates backend/frontend transport preparation: * * 1. Find circular or repeated object references. * 2. Replace subsequent references with null. * 3. Return JSON-safe data and circular mapping. */ function serializeWithCircs(input, options = {}) { const circs = lodash_walk_object_1.walk.Object(input, () => { }, { checkCircural: true, breadthWalk: options.breadthWalk ?? true, considerSharedObjects: options.considerSharedObjects ?? false, walkGetters: false, }).circs ?? []; /* * Clone using the mappings instead of JSON.stringify(input), * because the original object may contain real cycles. */ const data = Array.isArray(input) ? [] : {}; lodash_walk_object_1.walk.Object(input, (value, lodashPath, _changeValue, iteratorOptions) => { if (iteratorOptions?.isCircural) { lib_1._.set(data, lodashPath, null); return; } /* * Setting an object value can temporarily copy a reference from the * original graph. Its children will subsequently be populated by * the walker. This mirrors the behavior in EntityProcess.send(). */ lib_1._.set(data, lodashPath, value); }, { checkCircural: true, breadthWalk: options.breadthWalk ?? true, considerSharedObjects: options.considerSharedObjects ?? false, walkGetters: false, }); /* * Make the result truly transport-safe and independent from the source. * All repeated/circular locations should already be null. */ const jsonSafeData = JSON.parse(JSON.stringify(data)); return { data: jsonSafeData, circs, }; } /** * Recreates object references after JSON transport. * * Important: * mappings must be restored in an order where the target path already exists. */ function restoreCircs(transportedData, circs) { for (const circ of circs) { const targetPath = circ.circuralTargetPath; const target = targetPath === '' ? transportedData : lib_1._.get(transportedData, targetPath); if (target === null || (typeof target !== 'object' && typeof target !== 'function')) { throw new Error(`Cannot restore reference "${circ.pathToObj}": ` + `target "${targetPath}" is not an object.`); } lib_1._.set(transportedData, circ.pathToObj, target); } return transportedData; } function transportRoundTrip(input, options = {}) { const serialized = serializeWithCircs(input, options); /* * Simulates sending body and header through HTTP. */ const responseBody = JSON.parse(JSON.stringify(serialized.data)); const responseHeader = JSON.parse(JSON.stringify(serialized.circs)); return { serialized, restored: restoreCircs(responseBody, responseHeader), }; } describe('circular object transport', () => { it('throws when the circular target path is not an object', () => { const transported = { id: 1, invalidTarget: 'not-an-object', relation: null, }; expect(() => { restoreCircs(transported, [ { pathToObj: 'relation', circuralTargetPath: 'invalidTarget', }, ]); }).toThrow('Cannot restore reference "relation": ' + 'target "invalidTarget" is not an object.'); }); it('throws when the circular target path does not exist', () => { const transported = { id: 1, relation: null, }; expect(() => { restoreCircs(transported, [ { pathToObj: 'relation', circuralTargetPath: 'missing.path', }, ]); }).toThrow('Cannot restore reference "relation": ' + 'target "missing.path" is not an object.'); }); it('throws when the circular target is null', () => { const transported = { invalidTarget: null, relation: null, }; expect(() => { restoreCircs(transported, [ { pathToObj: 'relation', circuralTargetPath: 'invalidTarget', }, ]); }).toThrow('Cannot restore reference "relation": ' + 'target "invalidTarget" is not an object.'); }); it('treats an empty circular target path as the root object', () => { const transported = { id: 1, self: null, }; const restored = restoreCircs(transported, [ { pathToObj: 'self', circuralTargetPath: '', }, ]); expect(restored.self).toBe(restored); }); it('treats an empty circular target path as the root object', () => { const transported = { id: 1, self: null, }; const restored = restoreCircs(transported, [ { pathToObj: 'self', circuralTargetPath: '', }, ]); expect(restored.self).toBe(restored); }); describe('single object', () => { it('recreates a direct self-reference', () => { const input = { id: 1, name: 'root', }; input.self = input; const { serialized, restored } = transportRoundTrip(input); expect(serialized.data).toEqual({ id: 1, name: 'root', self: null, }); expect(serialized.circs).toEqual([ { pathToObj: 'self', circuralTargetPath: '', }, ]); expect(restored.self).toBe(restored); expect(restored.self?.id).toBe(1); }); it('recreates a nested child-to-parent reference', () => { const parent = { id: 1, }; const child = { id: 2, parent, }; parent.child = child; const { serialized, restored } = transportRoundTrip(parent); expect(serialized.data).toEqual({ id: 1, child: { id: 2, parent: null, }, }); expect(serialized.circs).toContainEqual({ pathToObj: 'child.parent', circuralTargetPath: '', }); expect(restored.child?.parent).toBe(restored); expect(restored.child?.parent.child).toBe(restored.child); }); it('recreates a deeper cycle', () => { const first = { id: 'first', }; const second = { id: 'second', }; const third = { id: 'third', }; first.next = second; second.next = third; third.next = first; const { restored, serialized } = transportRoundTrip(first); expect(serialized.circs).toContainEqual({ pathToObj: 'next.next.next', circuralTargetPath: '', }); expect(restored.next?.next?.next).toBe(restored); expect(restored.next?.next?.next?.next).toBe(restored.next); }); }); describe('array of entities', () => { it('recreates a reference from the second object to the first', () => { const first = { id: 1, }; const second = { id: 2, related: first, }; const input = [first, second]; const { serialized, restored } = transportRoundTrip(input); expect(serialized.data).toEqual([ { id: 1, }, { id: 2, related: null, }, ]); expect(serialized.circs).toContainEqual({ pathToObj: '[1].related', circuralTargetPath: '[0]', }); expect(restored[1].related).toBe(restored[0]); }); it('recreates bidirectional references between array elements', () => { const first = { id: 1, }; const second = { id: 2, }; first.other = second; second.other = first; const input = [first, second]; const { serialized, restored } = transportRoundTrip(input); /* * Depending on traversal order, second may appear first under * [0].other and the root [1] occurrence may be replaced with null. */ expect(serialized.circs.length).toBeGreaterThan(0); expect(restored[0].other).toBe(restored[1]); expect(restored[1].other).toBe(restored[0]); }); it('handles an array element represented as null in the response body', () => { const first = { id: 1, }; const second = { id: 2, dependency: first, }; /* * first already contains second, so traversal can first encounter * second through first.dependency-like paths depending on graph shape. */ first.dependency = second; const input = [first, second]; const { serialized, restored } = transportRoundTrip(input); const rootArrayMapping = serialized.circs.find(circ => circ.pathToObj === '[1]'); if (rootArrayMapping) { expect(serialized.data[1]).toBeNull(); } expect(restored[0].dependency).toBe(restored[1]); expect(restored[1].dependency).toBe(restored[0]); }); it('recreates several objects referencing the same array entity', () => { const owner = { id: 1, }; const firstChild = { id: 2, owner, }; const secondChild = { id: 3, owner, }; const input = [owner, firstChild, secondChild]; const { serialized, restored } = transportRoundTrip(input); expect(serialized.circs).toEqual(expect.arrayContaining([ { pathToObj: '[1].owner', circuralTargetPath: '[0]', }, { pathToObj: '[2].owner', circuralTargetPath: '[0]', }, ])); expect(restored[1].owner).toBe(restored[0]); expect(restored[2].owner).toBe(restored[0]); expect(restored[1].owner).toBe(restored[2].owner); }); }); describe('shared object behavior', () => { it('restores shared references with default semantics', () => { const shared = { value: 123, }; const input = { first: shared, second: shared, third: shared, }; const { serialized, restored } = transportRoundTrip(input); expect(serialized.data).toEqual({ first: { value: 123, }, second: null, third: null, }); expect(serialized.circs).toEqual([ { pathToObj: 'second', circuralTargetPath: 'first', }, { pathToObj: 'third', circuralTargetPath: 'first', }, ]); expect(restored.second).toBe(restored.first); expect(restored.third).toBe(restored.first); }); it('duplicates shared objects when considerSharedObjects is enabled', () => { const shared = { value: 123, }; const input = { first: shared, second: shared, }; const { serialized, restored } = transportRoundTrip(input, { considerSharedObjects: true, breadthWalk: false, }); expect(serialized.circs).toEqual([]); expect(restored).toEqual({ first: { value: 123, }, second: { value: 123, }, }); /* * JSON transport creates two independent objects because no mapping * was emitted for the shared reference. */ expect(restored.second).not.toBe(restored.first); }); }); describe('transport safety', () => { it('produces JSON-stringifiable body and headers', () => { const input = { id: 1, }; input.parent = input; const serialized = serializeWithCircs(input); expect(() => { JSON.stringify(serialized.data); }).not.toThrow(); expect(() => { JSON.stringify(serialized.circs); }).not.toThrow(); }); it('does not retain references to the source graph', () => { const input = { id: 1, }; input.self = input; const { restored } = transportRoundTrip(input); expect(restored).not.toBe(input); expect(restored.self).not.toBe(input); expect(restored.self).toBe(restored); }); it('preserves primitive and null values', () => { const input = { id: 123, text: 'hello', enabled: false, nullable: null, optional: undefined, }; input.self = input; const { restored } = transportRoundTrip(input); expect(restored.id).toBe(123); expect(restored.text).toBe('hello'); expect(restored.enabled).toBe(false); expect(restored.nullable).toBeNull(); /* * undefined is normally removed by JSON transport. */ expect('optional' in restored).toBe(false); expect(restored.self).toBe(restored); }); }); }); //# sourceMappingURL=backend-frontend-communication.test.js.map