diff --git a/src/lib/object-diff/index.ts b/src/lib/object-diff/index.ts index 524e9cd..af03b4b 100644 --- a/src/lib/object-diff/index.ts +++ b/src/lib/object-diff/index.ts @@ -14,21 +14,19 @@ function getLeanDiff( showOnly: ObjectDiffOptions["showOnly"] = DEFAULT_OBJECT_DIFF_OPTIONS.showOnly, ): ObjectDiff["diff"] { const { statuses, granularity } = showOnly; - return diff.reduce( - (acc, value) => { - if (granularity === GRANULARITY.DEEP && value.diff) { - const leanDiff = getLeanDiff(value.diff, showOnly); - if (leanDiff.length > 0) { - return [...acc, { ...value, diff: leanDiff }]; - } + const res: ObjectDiff["diff"] = []; + for (let i = 0; i < diff.length; i++) { + const value = diff[i]; + if (granularity === GRANULARITY.DEEP && value.diff) { + const leanDiff = getLeanDiff(value.diff, showOnly); + if (leanDiff.length > 0) { + res.push({ ...value, diff: leanDiff }); } - if (statuses.includes(value.status)) { - return [...acc, value]; - } - return acc; - }, - [] as ObjectDiff["diff"], - ); + } else if (statuses.includes(value.status)) { + res.push(value); + } + } + return res; } function getObjectStatus(diff: ObjectDiff["diff"]): OBJECT_STATUS { @@ -50,18 +48,19 @@ function formatSingleObjectDiff( }; } const diff: ObjectDiff["diff"] = []; - Object.entries(data).forEach(([property, value]) => { + + for (const [property, value] of Object.entries(data)) { if (isObject(value)) { const subPropertiesDiff: Diff[] = []; - Object.entries(value).forEach(([subProperty, subValue]) => { + for (const [subProperty, subValue] of Object.entries(value)) { subPropertiesDiff.push({ property: subProperty, previousValue: status === OBJECT_STATUS.ADDED ? undefined : subValue, currentValue: status === OBJECT_STATUS.ADDED ? subValue : undefined, status, }); - }); - return diff.push({ + } + diff.push({ property, previousValue: status === OBJECT_STATUS.ADDED ? undefined : data[property], @@ -69,15 +68,17 @@ function formatSingleObjectDiff( status, diff: subPropertiesDiff, }); + } else { + diff.push({ + property, + previousValue: + status === OBJECT_STATUS.ADDED ? undefined : data[property], + currentValue: status === OBJECT_STATUS.ADDED ? value : undefined, + status, + }); } - return diff.push({ - property, - previousValue: - status === OBJECT_STATUS.ADDED ? undefined : data[property], - currentValue: status === OBJECT_STATUS.ADDED ? value : undefined, - status, - }); - }); + } + if (options.showOnly && options.showOnly.statuses.length > 0) { return { type: "object", @@ -92,20 +93,6 @@ function formatSingleObjectDiff( }; } -function getPreviousMatch( - previousValue: unknown | undefined, - nextSubProperty: unknown, - options?: ObjectDiffOptions, -): unknown | undefined { - if (!previousValue) { - return undefined; - } - const previousMatch = Object.entries(previousValue).find(([subPreviousKey]) => - isEqual(subPreviousKey, nextSubProperty, options), - ); - return previousMatch ? previousMatch[1] : undefined; -} - function getValueStatus( previousValue: unknown, nextValue: unknown, @@ -117,92 +104,61 @@ function getValueStatus( return OBJECT_STATUS.UPDATED; } -function getPropertyStatus(subPropertiesDiff: Diff[]): OBJECT_STATUS { - return subPropertiesDiff.some( - (property) => property.status !== OBJECT_STATUS.EQUAL, - ) - ? OBJECT_STATUS.UPDATED - : OBJECT_STATUS.EQUAL; -} - -function getDeletedProperties( - previousValue: Record | undefined, - nextValue: Record, -): { property: string; value: unknown }[] | undefined { - if (!previousValue) return undefined; - const prevKeys = Object.keys(previousValue); - const nextKeys = Object.keys(nextValue); - const deletedKeys = prevKeys.filter((prevKey) => !nextKeys.includes(prevKey)); - if (deletedKeys.length > 0) { - return deletedKeys.map((deletedKey) => ({ - property: deletedKey, - value: previousValue[deletedKey], - })); - } - return undefined; -} - -function getSubPropertiesDiff( - previousValue: Record | undefined, +function getDiff( + previousValue: Record | undefined = {}, nextValue: Record, options?: ObjectDiffOptions, ): Diff[] { - const subPropertiesDiff: Diff[] = []; - let subDiff: Diff[]; - const deletedMainSubProperties = getDeletedProperties( - previousValue, - nextValue, - ); - if (deletedMainSubProperties) { - deletedMainSubProperties.forEach((deletedProperty) => { - subPropertiesDiff.push({ - property: deletedProperty.property, - previousValue: deletedProperty.value, + const diff: Diff[] = []; + const allKeys = new Set([ + ...Object.keys(previousValue), + ...Object.keys(nextValue), + ]); + + for (const property of allKeys) { + const prevSubValue = previousValue[property]; + const nextSubValue = nextValue[property]; + if (!(property in nextValue)) { + diff.push({ + property, + previousValue: prevSubValue, currentValue: undefined, status: OBJECT_STATUS.DELETED, }); - }); - } - Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { - const previousMatch = getPreviousMatch( - previousValue, - nextSubProperty, - options, - ); - if (!previousMatch) { - return subPropertiesDiff.push({ - property: nextSubProperty, - previousValue: previousMatch, + continue; + } + if (!(property in previousValue)) { + diff.push({ + property, + previousValue: undefined, currentValue: nextSubValue, - status: - !previousValue || !(nextSubProperty in previousValue) - ? OBJECT_STATUS.ADDED - : previousMatch === nextSubValue - ? OBJECT_STATUS.EQUAL - : OBJECT_STATUS.UPDATED, + status: OBJECT_STATUS.ADDED, }); + continue; } - if (isObject(nextSubValue)) { - const data: Diff[] = getSubPropertiesDiff( - previousMatch as Record, - nextSubValue, - options, + if (isObject(nextSubValue) && isObject(prevSubValue)) { + const subDiff = getDiff(prevSubValue, nextSubValue, options); + const isUpdated = subDiff.some( + (entry) => entry.status !== OBJECT_STATUS.EQUAL, ); - if (data && data.length > 0) { - subDiff = data; - } - } - if (previousMatch) { - subPropertiesDiff.push({ - property: nextSubProperty, - previousValue: previousMatch, + diff.push({ + property, + previousValue: prevSubValue, + currentValue: nextSubValue, + status: isUpdated ? OBJECT_STATUS.UPDATED : OBJECT_STATUS.EQUAL, + ...(isUpdated && { diff: subDiff }), + }); + } else { + const status = getValueStatus(prevSubValue, nextSubValue, options); + diff.push({ + property, + previousValue: prevSubValue, currentValue: nextSubValue, - status: getValueStatus(previousMatch, nextSubValue, options), - ...(!!subDiff && { diff: subDiff }), + status, }); } - }); - return subPropertiesDiff; + } + return diff; } /** @@ -234,66 +190,12 @@ export function getObjectDiff( if (!nextData) { return formatSingleObjectDiff(prevData, OBJECT_STATUS.DELETED, options); } - const diff: ObjectDiff["diff"] = []; - Object.entries(nextData).forEach(([nextProperty, nextValue]) => { - const previousValue = prevData[nextProperty]; - if (!previousValue) { - return diff.push({ - property: nextProperty, - previousValue, - currentValue: nextValue, - status: !(nextProperty in prevData) - ? OBJECT_STATUS.ADDED - : previousValue === nextValue - ? OBJECT_STATUS.EQUAL - : OBJECT_STATUS.UPDATED, - }); - } - if (isObject(nextValue)) { - const subPropertiesDiff: Diff[] = getSubPropertiesDiff( - previousValue as Record, - nextValue, - options, - ); - const subPropertyStatus = getPropertyStatus(subPropertiesDiff); - return diff.push({ - property: nextProperty, - previousValue, - currentValue: nextValue, - status: subPropertyStatus, - ...(subPropertyStatus !== OBJECT_STATUS.EQUAL && { - diff: subPropertiesDiff, - }), - }); - } - return diff.push({ - property: nextProperty, - previousValue, - currentValue: nextValue, - status: getValueStatus(previousValue, nextValue, options), - }); - }); - const deletedProperties = getDeletedProperties(prevData, nextData); - if (deletedProperties) { - deletedProperties.forEach((deletedProperty) => { - diff.push({ - property: deletedProperty.property, - previousValue: deletedProperty.value, - currentValue: undefined, - status: OBJECT_STATUS.DELETED, - }); - }); - } - if (options.showOnly && options.showOnly.statuses.length > 0) { - return { - type: "object", - status: getObjectStatus(diff), - diff: getLeanDiff(diff, options.showOnly), - }; - } + const diff: ObjectDiff["diff"] = getDiff(prevData, nextData, options); + const status = getObjectStatus(diff); + const showLeanDiff = (options?.showOnly?.statuses?.length || 0) > 0; return { type: "object", - status: getObjectStatus(diff), - diff, + status, + diff: showLeanDiff ? getLeanDiff(diff, options.showOnly) : diff, }; } diff --git a/src/lib/object-diff/object-diff.test.ts b/src/lib/object-diff/object-diff.test.ts index 62295c3..78fea53 100644 --- a/src/lib/object-diff/object-diff.test.ts +++ b/src/lib/object-diff/object-diff.test.ts @@ -40,6 +40,28 @@ describe("getObjectDiff", () => { ], }); }); + it("consider previous object as completely deleted if no next object is provided, and return an empty diff if showOnly doesn't require deleted values", () => { + expect( + getObjectDiff( + { + name: "joe", + age: 54, + hobbies: ["golf", "football"], + }, + null, + { + showOnly: { + statuses: [OBJECT_STATUS.ADDED], + granularity: GRANULARITY.DEEP, + }, + }, + ), + ).toStrictEqual({ + type: "object", + status: "deleted", + diff: [], + }); + }); it("consider next object as completely added if no previous object is provided", () => { expect( getObjectDiff(null, { @@ -170,10 +192,10 @@ describe("getObjectDiff", () => { status: "equal", }, { - property: "country", - previousValue: undefined, - currentValue: "us", - status: "added", + property: "type", + previousValue: "sport", + currentValue: undefined, + status: "deleted", }, { property: "user", @@ -191,12 +213,6 @@ describe("getObjectDiff", () => { }, status: "updated", diff: [ - { - property: "age", - previousValue: 66, - currentValue: undefined, - status: "deleted", - }, { property: "name", previousValue: "joe", @@ -215,6 +231,12 @@ describe("getObjectDiff", () => { currentValue: ["golf", "chess"], status: "updated", }, + { + property: "age", + previousValue: 66, + currentValue: undefined, + status: "deleted", + }, { property: "nickname", previousValue: undefined, @@ -224,10 +246,10 @@ describe("getObjectDiff", () => { ], }, { - property: "type", - previousValue: "sport", - currentValue: undefined, - status: "deleted", + property: "country", + previousValue: undefined, + currentValue: "us", + status: "added", }, ], }); @@ -338,18 +360,18 @@ describe("getObjectDiff", () => { }, status: "updated", diff: [ - { - property: "rugby", - previousValue: ["france"], - currentValue: undefined, - status: "deleted", - }, { property: "football", previousValue: ["psg"], currentValue: ["psg", "nantes"], status: "updated", }, + { + property: "rugby", + previousValue: ["france"], + currentValue: undefined, + status: "deleted", + }, { property: "golf", previousValue: undefined, @@ -401,11 +423,12 @@ describe("getObjectDiff", () => { status: "equal", }, { - property: "country", - previousValue: undefined, - currentValue: "us", - status: "added", + property: "type", + previousValue: "sport", + currentValue: undefined, + status: "deleted", }, + { property: "user", previousValue: { @@ -422,12 +445,6 @@ describe("getObjectDiff", () => { }, status: "updated", diff: [ - { - property: "age", - previousValue: 66, - currentValue: undefined, - status: "deleted", - }, { property: "name", previousValue: "joe", @@ -446,6 +463,12 @@ describe("getObjectDiff", () => { currentValue: ["football", "golf"], status: "equal", }, + { + property: "age", + previousValue: 66, + currentValue: undefined, + status: "deleted", + }, { property: "nickname", previousValue: undefined, @@ -455,10 +478,10 @@ describe("getObjectDiff", () => { ], }, { - property: "type", - previousValue: "sport", - currentValue: undefined, - status: "deleted", + property: "country", + previousValue: undefined, + currentValue: "us", + status: "added", }, ], }); @@ -536,10 +559,10 @@ describe("getObjectDiff", () => { status: "updated", diff: [ { - property: "country", - previousValue: undefined, - currentValue: "us", - status: "added", + property: "type", + previousValue: "sport", + currentValue: undefined, + status: "deleted", }, { property: "user", @@ -572,10 +595,10 @@ describe("getObjectDiff", () => { ], }, { - property: "type", - previousValue: "sport", - currentValue: undefined, - status: "deleted", + property: "country", + previousValue: undefined, + currentValue: "us", + status: "added", }, ], }); @@ -822,26 +845,6 @@ describe("getObjectDiff", () => { diff: [], }); }); - expect( - getObjectDiff( - { - name: "joe", - age: 54, - hobbies: ["golf", "football"], - }, - null, - { - showOnly: { - statuses: [OBJECT_STATUS.ADDED], - granularity: GRANULARITY.DEEP, - }, - }, - ), - ).toStrictEqual({ - type: "object", - status: "deleted", - diff: [], - }); it("returns all values if their status match the required statuses", () => { expect( getObjectDiff(