From 0c59d87e358c48d804ec4a5a7e059481bbe64c13 Mon Sep 17 00:00:00 2001 From: Marco Link Date: Thu, 5 Sep 2024 00:32:27 +0200 Subject: [PATCH] fix: ignore key order when comparing objects at max depth --- src/index.spec.ts | 37 ++++++++++++++++++++++++++++++++----- src/index.ts | 25 ++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/index.spec.ts b/src/index.spec.ts index 84d35fa..0370039 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -679,7 +679,7 @@ describe('a generate json patch function', () => { }, }; - it('detects changes as a given depth of 3', () => { + it('detects changes at a given depth of 3', () => { const patch = generateJSONPatch(before, after, { maxDepth: 3 }); expect(patch).to.eql([ { @@ -737,7 +737,7 @@ describe('a generate json patch function', () => { expect(patch).to.eql([]); }); - it('detects changes as a given depth of 4', () => { + it('detects changes at a given depth of 4', () => { const afterModified = structuredClone(after); afterModified.firstLevel.secondLevel.thirdLevelTwo = 'hello-world'; const patch = generateJSONPatch(before, afterModified, { maxDepth: 4 }); @@ -757,7 +757,7 @@ describe('a generate json patch function', () => { ]); }); - it('detects changes as a given depth of 4 for an array value', () => { + it('detects changes at a given depth of 4 for an array value', () => { const afterModified = structuredClone(before); afterModified.firstLevel.secondLevel.thirdLevelThree = ['test']; const patch = generateJSONPatch(before, afterModified, { maxDepth: 4 }); @@ -770,7 +770,7 @@ describe('a generate json patch function', () => { ]); }); - it('detects changes as a given depth of 4 for an removed array value', () => { + it('detects changes as a given depth of 4 for a removed array value', () => { const afterModified = structuredClone(before); // @ts-ignore delete afterModified.firstLevel.secondLevel.thirdLevelThree; @@ -783,7 +783,7 @@ describe('a generate json patch function', () => { ]); }); - it('detects changes as a given depth of 4 for an nullyfied array value', () => { + it('detects changes as a given depth of 4 for a nullyfied array value', () => { const afterModified = structuredClone(before); // @ts-ignore afterModified.firstLevel.secondLevel.thirdLevelThree = null; @@ -796,6 +796,33 @@ describe('a generate json patch function', () => { }, ]); }); + + it('ignores key order on objects when comparing at max depth', () => { + const before = { + a: { + b: { + c: { + d: 'hello', + e: 'world', + }, + }, + }, + }; + + const after = { + a: { + b: { + c: { + e: 'world', + d: 'hello', + }, + }, + }, + }; + + const patch = generateJSONPatch(before, after, { maxDepth: 1 }); + expect(patch).to.eql([]); + }); }); }); diff --git a/src/index.ts b/src/index.ts index 9bb3237..9cc3859 100644 --- a/src/index.ts +++ b/src/index.ts @@ -198,7 +198,7 @@ export function generateJSONPatch( } else if (isJsonObject(rightValue)) { if (isJsonObject(leftValue)) { if (maxDepthReached(newPath)) { - if (JSON.stringify(leftValue) !== JSON.stringify(rightValue)) { + if (!deepEqual(leftValue, rightValue)) { patch.push({ op: 'replace', path: newPath, value: rightValue }); } } else { @@ -251,6 +251,29 @@ function isJsonObject(value: JsonValue): value is JsonObject { return value?.constructor === Object; } +function deepEqual(objA: any, objB: any) { + return stringifySorted(objA) === stringifySorted(objB); +} + +function stringifySorted(obj: any): string { + if (typeof obj !== 'object' || obj === null) { + return JSON.stringify(obj); + } + + if (Array.isArray(obj)) { + return JSON.stringify(obj.map((item) => stringifySorted(item))); + } + + const sortedObj: Record = {}; + const sortedKeys = Object.keys(obj).sort(); + + sortedKeys.forEach((key) => { + sortedObj[key] = stringifySorted(obj[key]); + }); + + return JSON.stringify(sortedObj); +} + export type PathInfoResult = { segments: string[]; length: number;