From 813ef278c1933e01df5a8ec7f61979609a3fe8d7 Mon Sep 17 00:00:00 2001 From: DoneDeal0 Date: Fri, 23 Dec 2022 22:37:06 +0100 Subject: [PATCH 1/2] feat: start deep nested diff --- src/model.ts | 1 + src/object-diff.ts | 72 +++++--- test/object-diff.test.ts | 356 ++++++++++++++++++++++++++++----------- 3 files changed, 311 insertions(+), 118 deletions(-) diff --git a/src/model.ts b/src/model.ts index 18b896c..595d83b 100644 --- a/src/model.ts +++ b/src/model.ts @@ -27,6 +27,7 @@ export type Subproperties = { previousValue: any; currentValue: any; status: DiffStatus; + subDiff?: Subproperties[]; }; export type ObjectDiff = { diff --git a/src/object-diff.ts b/src/object-diff.ts index 532a394..3893619 100644 --- a/src/object-diff.ts +++ b/src/object-diff.ts @@ -58,6 +58,53 @@ function formatSingleObjectDiff( }; } +function getPreviousMatch( + prevSubValues: [string, any][] | null, + nextSubProperty: any +): any | undefined { + const previousMatch = + prevSubValues && + prevSubValues.find(([subPreviousKey]) => + isEqual(subPreviousKey, nextSubProperty) + ); + return previousMatch ? previousMatch[1] : undefined; +} + +function getSubPropertiesDiff( + previousValue: Record | undefined, + nextValue: Record +): Subproperties[] { + const subPropertiesDiff: Subproperties[] = []; + const prevSubValues = previousValue ? Object.entries(previousValue) : null; + let subDiff: Subproperties["subDiff"]; + Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { + const previousMatch = getPreviousMatch(prevSubValues, nextSubProperty); + if (isObject(nextSubValue)) { + const data: Subproperties[] = getSubPropertiesDiff( + previousMatch, + nextSubValue + ); + if (data && data.length > 0) { + subDiff = data; + } + } + if (prevSubValues) { + if (previousMatch) { + subPropertiesDiff.push({ + name: nextSubProperty, + previousValue: previousMatch, + currentValue: nextSubValue, + status: isEqual(previousMatch, nextSubValue) + ? STATUS.EQUAL + : STATUS.UPDATED, + ...(!!subDiff && { subDiff }), + }); + } + } + }); + return subPropertiesDiff; +} + export function getObjectDiff( prevData: ObjectData, nextData: ObjectData @@ -80,27 +127,10 @@ export function getObjectDiff( const previousValue = prevData[nextProperty]; if (isObject(nextValue)) { - const prevSubValues = previousValue - ? Object.entries(previousValue) - : null; - const subPropertiesDiff: Subproperties[] = []; - Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { - if (prevSubValues) { - const previousMatch = prevSubValues.find(([subPreviousKey]) => - isEqual(subPreviousKey, nextSubProperty) - ); - if (previousMatch) { - subPropertiesDiff.push({ - name: nextSubProperty, - previousValue: previousMatch[1], - currentValue: nextSubValue, - status: isEqual(previousMatch[1], nextSubValue) - ? STATUS.EQUAL - : STATUS.UPDATED, - }); - } - } - }); + const subPropertiesDiff: Subproperties[] = getSubPropertiesDiff( + previousValue, + nextValue + ); const _status = subPropertiesDiff.some( (property) => property.status !== STATUS.EQUAL ) diff --git a/test/object-diff.test.ts b/test/object-diff.test.ts index 071c416..a65b170 100644 --- a/test/object-diff.test.ts +++ b/test/object-diff.test.ts @@ -1,95 +1,215 @@ import { getObjectDiff } from "../src/object-diff"; describe("getObjectDiff", () => { - it("returns an empty diff if no objects are provided", () => { - expect(getObjectDiff(null, null)).toStrictEqual({ - type: "object", - status: "equal", - diff: [], - }); - }); - it("consider previous object as completely deleted if no next object is provided", () => { - expect( - getObjectDiff( - { name: "joe", age: 54, hobbies: ["golf", "football"] }, - null + // it("returns an empty diff if no objects are provided", () => { + // expect(getObjectDiff(null, null)).toStrictEqual({ + // type: "object", + // status: "equal", + // diff: [], + // }); + // }); + // it("consider previous object as completely deleted if no next object is provided", () => { + // expect( + // getObjectDiff( + // { name: "joe", age: 54, hobbies: ["golf", "football"] }, + // null + // ) + // ).toStrictEqual({ + // type: "object", + // status: "deleted", + // diff: [ + // { + // property: "name", + // previousValue: "joe", + // currentValue: undefined, + // status: "deleted", + // }, + // { + // property: "age", + // previousValue: 54, + // currentValue: undefined, + // status: "deleted", + // }, + // { + // property: "hobbies", + // previousValue: ["golf", "football"], + // currentValue: undefined, + // status: "deleted", + // }, + // ], + // }); + // }); + // it("consider next object as completely added if no previous object is provided", () => { + // expect( + // getObjectDiff(null, { + // name: "joe", + // age: 54, + // hobbies: ["golf", "football"], + // }) + // ).toStrictEqual({ + // type: "object", + // status: "added", + // diff: [ + // { + // property: "name", + // previousValue: undefined, + // currentValue: "joe", + // status: "added", + // }, + // { + // property: "age", + // previousValue: undefined, + // currentValue: 54, + // status: "added", + // }, + // { + // property: "hobbies", + // previousValue: undefined, + // currentValue: ["golf", "football"], + // status: "added", + // }, + // ], + // }); + // }); + // it("detects changed between two objects", () => { + // expect( + // getObjectDiff( + // { + // id: 54, + // user: { + // name: "joe", + // member: true, + // hobbies: ["golf", "football"], + // age: 66, + // }, + // }, + // { + // id: 54, + // user: { + // name: "joe", + // member: false, + // hobbies: ["golf", "chess"], + // age: 66, + // }, + // } + // ) + // ).toStrictEqual({ + // type: "object", + // status: "updated", + // diff: [ + // { + // property: "id", + // previousValue: 54, + // currentValue: 54, + // status: "equal", + // }, + // { + // property: "user", + // previousValue: { + // name: "joe", + // member: true, + // hobbies: ["golf", "football"], + // age: 66, + // }, + // currentValue: { + // name: "joe", + // member: false, + // hobbies: ["golf", "chess"], + // age: 66, + // }, + // status: "updated", + // subPropertiesDiff: [ + // { + // name: "name", + // previousValue: "joe", + // currentValue: "joe", + // status: "equal", + // }, + // { + // name: "member", + // previousValue: true, + // currentValue: false, + // status: "updated", + // }, + // { + // name: "hobbies", + // previousValue: ["golf", "football"], + // currentValue: ["golf", "chess"], + // status: "updated", + // }, + // { + // name: "age", + // previousValue: 66, + // currentValue: 66, + // status: "equal", + // }, + // ], + // }, + // ], + // }); + // }); + it("detects changed between two deep nested objects", () => { + console.log( + "res", + JSON.stringify( + getObjectDiff( + { + id: 54, + user: { + name: "joe", + data: { + member: true, + hobbies: { + football: ["psg"], + rugby: ["france"], + }, + }, + }, + }, + { + id: 54, + user: { + name: "joe", + data: { + member: true, + hobbies: { + football: ["psg", "nantes"], + rugby: ["france"], + }, + }, + }, + } + ), + null, + 2 ) - ).toStrictEqual({ - type: "object", - status: "deleted", - diff: [ - { - property: "name", - previousValue: "joe", - currentValue: undefined, - status: "deleted", - }, - { - property: "age", - previousValue: 54, - currentValue: undefined, - status: "deleted", - }, - { - property: "hobbies", - previousValue: ["golf", "football"], - currentValue: undefined, - status: "deleted", - }, - ], - }); - }); - it("consider next object as completely added if no previous object is provided", () => { - expect( - getObjectDiff(null, { - name: "joe", - age: 54, - hobbies: ["golf", "football"], - }) - ).toStrictEqual({ - type: "object", - status: "added", - diff: [ - { - property: "name", - previousValue: undefined, - currentValue: "joe", - status: "added", - }, - { - property: "age", - previousValue: undefined, - currentValue: 54, - status: "added", - }, - { - property: "hobbies", - previousValue: undefined, - currentValue: ["golf", "football"], - status: "added", - }, - ], - }); - }); - it("detects changed between two objects", () => { + ); expect( getObjectDiff( { id: 54, user: { name: "joe", - member: true, - hobbies: ["golf", "football"], - age: 66, + data: { + member: true, + hobbies: { + football: ["psg"], + rugby: ["france"], + }, + }, }, }, { id: 54, user: { name: "joe", - member: false, - hobbies: ["golf", "chess"], - age: 66, + data: { + member: true, + hobbies: { + football: ["psg", "nantes"], + rugby: ["france"], + }, + }, }, } ) @@ -107,15 +227,23 @@ describe("getObjectDiff", () => { property: "user", previousValue: { name: "joe", - member: true, - hobbies: ["golf", "football"], - age: 66, + data: { + member: true, + hobbies: { + football: ["psg"], + rugby: ["france"], + }, + }, }, currentValue: { name: "joe", - member: false, - hobbies: ["golf", "chess"], - age: 66, + data: { + member: true, + hobbies: { + football: ["psg", "nantes"], + rugby: ["france"], + }, + }, }, status: "updated", subPropertiesDiff: [ @@ -126,22 +254,56 @@ describe("getObjectDiff", () => { status: "equal", }, { - name: "member", - previousValue: true, - currentValue: false, + name: "data", + previousValue: { + member: true, + hobbies: { + football: ["psg"], + rugby: ["france"], + }, + }, + currentValue: { + member: true, + hobbies: { + football: ["psg", "nantes"], + rugby: ["france"], + }, + }, status: "updated", - }, - { - name: "hobbies", - previousValue: ["golf", "football"], - currentValue: ["golf", "chess"], - status: "updated", - }, - { - name: "age", - previousValue: 66, - currentValue: 66, - status: "equal", + subDiff: [ + { + name: "member", + previousValue: true, + currentValue: true, + status: "equal", + }, + { + name: "hobbies", + previousValue: { + football: ["psg"], + rugby: ["france"], + }, + currentValue: { + football: ["psg", "nantes"], + rugby: ["france"], + }, + status: "updated", + subDiff: [ + { + name: "football", + previousValue: ["psg"], + currentValue: ["psg", "nantes"], + status: "updated", // error the algo says it's equal... + }, + { + name: "rugby", + previousValue: ["france"], + currentValue: ["france"], + status: "equal", + }, + ], + }, + ], }, ], }, From ec1c7166b9b849beaee3152ad695e87e0ee9c465 Mon Sep 17 00:00:00 2001 From: DoneDeal0 Date: Fri, 23 Dec 2022 22:58:16 +0100 Subject: [PATCH 2/2] fix: deep equality check --- src/object-diff.ts | 33 ++-- src/utils.ts | 3 + test/object-diff.test.ts | 329 +++++++++++++++++---------------------- test/utils.test.ts | 1 + 4 files changed, 167 insertions(+), 199 deletions(-) diff --git a/src/object-diff.ts b/src/object-diff.ts index 3893619..d5189ff 100644 --- a/src/object-diff.ts +++ b/src/object-diff.ts @@ -62,11 +62,12 @@ function getPreviousMatch( prevSubValues: [string, any][] | null, nextSubProperty: any ): any | undefined { - const previousMatch = - prevSubValues && - prevSubValues.find(([subPreviousKey]) => - isEqual(subPreviousKey, nextSubProperty) - ); + if (!prevSubValues) { + return undefined; + } + const previousMatch = prevSubValues.find(([subPreviousKey]) => + isEqual(subPreviousKey, nextSubProperty) + ); return previousMatch ? previousMatch[1] : undefined; } @@ -88,18 +89,16 @@ function getSubPropertiesDiff( subDiff = data; } } - if (prevSubValues) { - if (previousMatch) { - subPropertiesDiff.push({ - name: nextSubProperty, - previousValue: previousMatch, - currentValue: nextSubValue, - status: isEqual(previousMatch, nextSubValue) - ? STATUS.EQUAL - : STATUS.UPDATED, - ...(!!subDiff && { subDiff }), - }); - } + if (previousMatch) { + subPropertiesDiff.push({ + name: nextSubProperty, + previousValue: previousMatch, + currentValue: nextSubValue, + status: isEqual(previousMatch, nextSubValue) + ? STATUS.EQUAL + : STATUS.UPDATED, + ...(!!subDiff && { subDiff }), + }); } }); return subPropertiesDiff; diff --git a/src/utils.ts b/src/utils.ts index 55681d5..c526893 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,9 @@ export function isEqual(a: any, b: any): boolean { if (typeof a !== typeof b) return false; if (Array.isArray(a)) { + if (a.length !== b.length) { + return false; + } return a.every((v, i) => JSON.stringify(v) === JSON.stringify(b[i])); } if (typeof a === "object") { diff --git a/test/object-diff.test.ts b/test/object-diff.test.ts index a65b170..c561d9a 100644 --- a/test/object-diff.test.ts +++ b/test/object-diff.test.ts @@ -1,189 +1,154 @@ import { getObjectDiff } from "../src/object-diff"; describe("getObjectDiff", () => { - // it("returns an empty diff if no objects are provided", () => { - // expect(getObjectDiff(null, null)).toStrictEqual({ - // type: "object", - // status: "equal", - // diff: [], - // }); - // }); - // it("consider previous object as completely deleted if no next object is provided", () => { - // expect( - // getObjectDiff( - // { name: "joe", age: 54, hobbies: ["golf", "football"] }, - // null - // ) - // ).toStrictEqual({ - // type: "object", - // status: "deleted", - // diff: [ - // { - // property: "name", - // previousValue: "joe", - // currentValue: undefined, - // status: "deleted", - // }, - // { - // property: "age", - // previousValue: 54, - // currentValue: undefined, - // status: "deleted", - // }, - // { - // property: "hobbies", - // previousValue: ["golf", "football"], - // currentValue: undefined, - // status: "deleted", - // }, - // ], - // }); - // }); - // it("consider next object as completely added if no previous object is provided", () => { - // expect( - // getObjectDiff(null, { - // name: "joe", - // age: 54, - // hobbies: ["golf", "football"], - // }) - // ).toStrictEqual({ - // type: "object", - // status: "added", - // diff: [ - // { - // property: "name", - // previousValue: undefined, - // currentValue: "joe", - // status: "added", - // }, - // { - // property: "age", - // previousValue: undefined, - // currentValue: 54, - // status: "added", - // }, - // { - // property: "hobbies", - // previousValue: undefined, - // currentValue: ["golf", "football"], - // status: "added", - // }, - // ], - // }); - // }); - // it("detects changed between two objects", () => { - // expect( - // getObjectDiff( - // { - // id: 54, - // user: { - // name: "joe", - // member: true, - // hobbies: ["golf", "football"], - // age: 66, - // }, - // }, - // { - // id: 54, - // user: { - // name: "joe", - // member: false, - // hobbies: ["golf", "chess"], - // age: 66, - // }, - // } - // ) - // ).toStrictEqual({ - // type: "object", - // status: "updated", - // diff: [ - // { - // property: "id", - // previousValue: 54, - // currentValue: 54, - // status: "equal", - // }, - // { - // property: "user", - // previousValue: { - // name: "joe", - // member: true, - // hobbies: ["golf", "football"], - // age: 66, - // }, - // currentValue: { - // name: "joe", - // member: false, - // hobbies: ["golf", "chess"], - // age: 66, - // }, - // status: "updated", - // subPropertiesDiff: [ - // { - // name: "name", - // previousValue: "joe", - // currentValue: "joe", - // status: "equal", - // }, - // { - // name: "member", - // previousValue: true, - // currentValue: false, - // status: "updated", - // }, - // { - // name: "hobbies", - // previousValue: ["golf", "football"], - // currentValue: ["golf", "chess"], - // status: "updated", - // }, - // { - // name: "age", - // previousValue: 66, - // currentValue: 66, - // status: "equal", - // }, - // ], - // }, - // ], - // }); - // }); - it("detects changed between two deep nested objects", () => { - console.log( - "res", - JSON.stringify( - getObjectDiff( - { - id: 54, - user: { - name: "joe", - data: { - member: true, - hobbies: { - football: ["psg"], - rugby: ["france"], - }, - }, - }, + it("returns an empty diff if no objects are provided", () => { + expect(getObjectDiff(null, null)).toStrictEqual({ + type: "object", + status: "equal", + diff: [], + }); + }); + it("consider previous object as completely deleted if no next object is provided", () => { + expect( + getObjectDiff( + { name: "joe", age: 54, hobbies: ["golf", "football"] }, + null + ) + ).toStrictEqual({ + type: "object", + status: "deleted", + diff: [ + { + property: "name", + previousValue: "joe", + currentValue: undefined, + status: "deleted", + }, + { + property: "age", + previousValue: 54, + currentValue: undefined, + status: "deleted", + }, + { + property: "hobbies", + previousValue: ["golf", "football"], + currentValue: undefined, + status: "deleted", + }, + ], + }); + }); + it("consider next object as completely added if no previous object is provided", () => { + expect( + getObjectDiff(null, { + name: "joe", + age: 54, + hobbies: ["golf", "football"], + }) + ).toStrictEqual({ + type: "object", + status: "added", + diff: [ + { + property: "name", + previousValue: undefined, + currentValue: "joe", + status: "added", + }, + { + property: "age", + previousValue: undefined, + currentValue: 54, + status: "added", + }, + { + property: "hobbies", + previousValue: undefined, + currentValue: ["golf", "football"], + status: "added", + }, + ], + }); + }); + it("detects changed between two objects", () => { + expect( + getObjectDiff( + { + id: 54, + user: { + name: "joe", + member: true, + hobbies: ["golf", "football"], + age: 66, }, - { - id: 54, - user: { - name: "joe", - data: { - member: true, - hobbies: { - football: ["psg", "nantes"], - rugby: ["france"], - }, - }, - }, - } - ), - null, - 2 + }, + { + id: 54, + user: { + name: "joe", + member: false, + hobbies: ["golf", "chess"], + age: 66, + }, + } ) - ); + ).toStrictEqual({ + type: "object", + status: "updated", + diff: [ + { + property: "id", + previousValue: 54, + currentValue: 54, + status: "equal", + }, + { + property: "user", + previousValue: { + name: "joe", + member: true, + hobbies: ["golf", "football"], + age: 66, + }, + currentValue: { + name: "joe", + member: false, + hobbies: ["golf", "chess"], + age: 66, + }, + status: "updated", + subPropertiesDiff: [ + { + name: "name", + previousValue: "joe", + currentValue: "joe", + status: "equal", + }, + { + name: "member", + previousValue: true, + currentValue: false, + status: "updated", + }, + { + name: "hobbies", + previousValue: ["golf", "football"], + currentValue: ["golf", "chess"], + status: "updated", + }, + { + name: "age", + previousValue: 66, + currentValue: 66, + status: "equal", + }, + ], + }, + ], + }); + }); + it("detects changed between two deep nested objects", () => { expect( getObjectDiff( { @@ -293,7 +258,7 @@ describe("getObjectDiff", () => { name: "football", previousValue: ["psg"], currentValue: ["psg", "nantes"], - status: "updated", // error the algo says it's equal... + status: "updated", }, { name: "rugby", diff --git a/test/utils.test.ts b/test/utils.test.ts index b751386..babe049 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -38,6 +38,7 @@ describe("isEqual", () => { ] ) ).toBeFalsy(); + expect(isEqual(["psg"], ["psg", "nantes"])).toBeFalsy(); }); });