Browse Source

chore: improve getobjectdiff performance (#30)

pull/31/head
DoneDeal0 6 months ago committed by GitHub
parent
commit
27306f7189
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 224
      src/lib/object-diff/index.ts
  2. 127
      src/lib/object-diff/object-diff.test.ts

224
src/lib/object-diff/index.ts

@ -14,21 +14,19 @@ function getLeanDiff(
showOnly: ObjectDiffOptions["showOnly"] = DEFAULT_OBJECT_DIFF_OPTIONS.showOnly, showOnly: ObjectDiffOptions["showOnly"] = DEFAULT_OBJECT_DIFF_OPTIONS.showOnly,
): ObjectDiff["diff"] { ): ObjectDiff["diff"] {
const { statuses, granularity } = showOnly; const { statuses, granularity } = showOnly;
return diff.reduce( const res: ObjectDiff["diff"] = [];
(acc, value) => { for (let i = 0; i < diff.length; i++) {
const value = diff[i];
if (granularity === GRANULARITY.DEEP && value.diff) { if (granularity === GRANULARITY.DEEP && value.diff) {
const leanDiff = getLeanDiff(value.diff, showOnly); const leanDiff = getLeanDiff(value.diff, showOnly);
if (leanDiff.length > 0) { if (leanDiff.length > 0) {
return [...acc, { ...value, diff: leanDiff }]; res.push({ ...value, diff: leanDiff });
} }
} else if (statuses.includes(value.status)) {
res.push(value);
} }
if (statuses.includes(value.status)) {
return [...acc, value];
} }
return acc; return res;
},
[] as ObjectDiff["diff"],
);
} }
function getObjectStatus(diff: ObjectDiff["diff"]): OBJECT_STATUS { function getObjectStatus(diff: ObjectDiff["diff"]): OBJECT_STATUS {
@ -50,18 +48,19 @@ function formatSingleObjectDiff(
}; };
} }
const diff: ObjectDiff["diff"] = []; const diff: ObjectDiff["diff"] = [];
Object.entries(data).forEach(([property, value]) => {
for (const [property, value] of Object.entries(data)) {
if (isObject(value)) { if (isObject(value)) {
const subPropertiesDiff: Diff[] = []; const subPropertiesDiff: Diff[] = [];
Object.entries(value).forEach(([subProperty, subValue]) => { for (const [subProperty, subValue] of Object.entries(value)) {
subPropertiesDiff.push({ subPropertiesDiff.push({
property: subProperty, property: subProperty,
previousValue: status === OBJECT_STATUS.ADDED ? undefined : subValue, previousValue: status === OBJECT_STATUS.ADDED ? undefined : subValue,
currentValue: status === OBJECT_STATUS.ADDED ? subValue : undefined, currentValue: status === OBJECT_STATUS.ADDED ? subValue : undefined,
status, status,
}); });
}); }
return diff.push({ diff.push({
property, property,
previousValue: previousValue:
status === OBJECT_STATUS.ADDED ? undefined : data[property], status === OBJECT_STATUS.ADDED ? undefined : data[property],
@ -69,15 +68,17 @@ function formatSingleObjectDiff(
status, status,
diff: subPropertiesDiff, diff: subPropertiesDiff,
}); });
} } else {
return diff.push({ diff.push({
property, property,
previousValue: previousValue:
status === OBJECT_STATUS.ADDED ? undefined : data[property], status === OBJECT_STATUS.ADDED ? undefined : data[property],
currentValue: status === OBJECT_STATUS.ADDED ? value : undefined, currentValue: status === OBJECT_STATUS.ADDED ? value : undefined,
status, status,
}); });
}); }
}
if (options.showOnly && options.showOnly.statuses.length > 0) { if (options.showOnly && options.showOnly.statuses.length > 0) {
return { return {
type: "object", 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( function getValueStatus(
previousValue: unknown, previousValue: unknown,
nextValue: unknown, nextValue: unknown,
@ -117,92 +104,61 @@ function getValueStatus(
return OBJECT_STATUS.UPDATED; return OBJECT_STATUS.UPDATED;
} }
function getPropertyStatus(subPropertiesDiff: Diff[]): OBJECT_STATUS { function getDiff(
return subPropertiesDiff.some( previousValue: Record<string, unknown> | undefined = {},
(property) => property.status !== OBJECT_STATUS.EQUAL,
)
? OBJECT_STATUS.UPDATED
: OBJECT_STATUS.EQUAL;
}
function getDeletedProperties(
previousValue: Record<string, unknown> | undefined,
nextValue: Record<string, unknown>,
): { 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<string, unknown> | undefined,
nextValue: Record<string, unknown>, nextValue: Record<string, unknown>,
options?: ObjectDiffOptions, options?: ObjectDiffOptions,
): Diff[] { ): Diff[] {
const subPropertiesDiff: Diff[] = []; const diff: Diff[] = [];
let subDiff: Diff[]; const allKeys = new Set([
const deletedMainSubProperties = getDeletedProperties( ...Object.keys(previousValue),
previousValue, ...Object.keys(nextValue),
nextValue, ]);
);
if (deletedMainSubProperties) { for (const property of allKeys) {
deletedMainSubProperties.forEach((deletedProperty) => { const prevSubValue = previousValue[property];
subPropertiesDiff.push({ const nextSubValue = nextValue[property];
property: deletedProperty.property, if (!(property in nextValue)) {
previousValue: deletedProperty.value, diff.push({
property,
previousValue: prevSubValue,
currentValue: undefined, currentValue: undefined,
status: OBJECT_STATUS.DELETED, status: OBJECT_STATUS.DELETED,
}); });
}); continue;
} }
Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => { if (!(property in previousValue)) {
const previousMatch = getPreviousMatch( diff.push({
previousValue, property,
nextSubProperty, previousValue: undefined,
options,
);
if (!previousMatch) {
return subPropertiesDiff.push({
property: nextSubProperty,
previousValue: previousMatch,
currentValue: nextSubValue, currentValue: nextSubValue,
status: status: OBJECT_STATUS.ADDED,
!previousValue || !(nextSubProperty in previousValue)
? OBJECT_STATUS.ADDED
: previousMatch === nextSubValue
? OBJECT_STATUS.EQUAL
: OBJECT_STATUS.UPDATED,
}); });
continue;
} }
if (isObject(nextSubValue)) { if (isObject(nextSubValue) && isObject(prevSubValue)) {
const data: Diff[] = getSubPropertiesDiff( const subDiff = getDiff(prevSubValue, nextSubValue, options);
previousMatch as Record<string, unknown>, const isUpdated = subDiff.some(
nextSubValue, (entry) => entry.status !== OBJECT_STATUS.EQUAL,
options,
); );
if (data && data.length > 0) { diff.push({
subDiff = data; property,
} previousValue: prevSubValue,
}
if (previousMatch) {
subPropertiesDiff.push({
property: nextSubProperty,
previousValue: previousMatch,
currentValue: nextSubValue, currentValue: nextSubValue,
status: getValueStatus(previousMatch, nextSubValue, options), status: isUpdated ? OBJECT_STATUS.UPDATED : OBJECT_STATUS.EQUAL,
...(!!subDiff && { diff: subDiff }), ...(isUpdated && { diff: subDiff }),
}); });
} } else {
const status = getValueStatus(prevSubValue, nextSubValue, options);
diff.push({
property,
previousValue: prevSubValue,
currentValue: nextSubValue,
status,
}); });
return subPropertiesDiff; }
}
return diff;
} }
/** /**
@ -234,66 +190,12 @@ export function getObjectDiff(
if (!nextData) { if (!nextData) {
return formatSingleObjectDiff(prevData, OBJECT_STATUS.DELETED, options); return formatSingleObjectDiff(prevData, OBJECT_STATUS.DELETED, options);
} }
const diff: ObjectDiff["diff"] = []; const diff: ObjectDiff["diff"] = getDiff(prevData, nextData, options);
Object.entries(nextData).forEach(([nextProperty, nextValue]) => { const status = getObjectStatus(diff);
const previousValue = prevData[nextProperty]; const showLeanDiff = (options?.showOnly?.statuses?.length || 0) > 0;
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<string, unknown>,
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),
};
}
return { return {
type: "object", type: "object",
status: getObjectStatus(diff), status,
diff, diff: showLeanDiff ? getLeanDiff(diff, options.showOnly) : diff,
}; };
} }

127
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", () => { it("consider next object as completely added if no previous object is provided", () => {
expect( expect(
getObjectDiff(null, { getObjectDiff(null, {
@ -170,10 +192,10 @@ describe("getObjectDiff", () => {
status: "equal", status: "equal",
}, },
{ {
property: "country", property: "type",
previousValue: undefined, previousValue: "sport",
currentValue: "us", currentValue: undefined,
status: "added", status: "deleted",
}, },
{ {
property: "user", property: "user",
@ -191,12 +213,6 @@ describe("getObjectDiff", () => {
}, },
status: "updated", status: "updated",
diff: [ diff: [
{
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{ {
property: "name", property: "name",
previousValue: "joe", previousValue: "joe",
@ -215,6 +231,12 @@ describe("getObjectDiff", () => {
currentValue: ["golf", "chess"], currentValue: ["golf", "chess"],
status: "updated", status: "updated",
}, },
{
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{ {
property: "nickname", property: "nickname",
previousValue: undefined, previousValue: undefined,
@ -224,10 +246,10 @@ describe("getObjectDiff", () => {
], ],
}, },
{ {
property: "type", property: "country",
previousValue: "sport", previousValue: undefined,
currentValue: undefined, currentValue: "us",
status: "deleted", status: "added",
}, },
], ],
}); });
@ -338,18 +360,18 @@ describe("getObjectDiff", () => {
}, },
status: "updated", status: "updated",
diff: [ diff: [
{
property: "rugby",
previousValue: ["france"],
currentValue: undefined,
status: "deleted",
},
{ {
property: "football", property: "football",
previousValue: ["psg"], previousValue: ["psg"],
currentValue: ["psg", "nantes"], currentValue: ["psg", "nantes"],
status: "updated", status: "updated",
}, },
{
property: "rugby",
previousValue: ["france"],
currentValue: undefined,
status: "deleted",
},
{ {
property: "golf", property: "golf",
previousValue: undefined, previousValue: undefined,
@ -401,11 +423,12 @@ describe("getObjectDiff", () => {
status: "equal", status: "equal",
}, },
{ {
property: "country", property: "type",
previousValue: undefined, previousValue: "sport",
currentValue: "us", currentValue: undefined,
status: "added", status: "deleted",
}, },
{ {
property: "user", property: "user",
previousValue: { previousValue: {
@ -422,12 +445,6 @@ describe("getObjectDiff", () => {
}, },
status: "updated", status: "updated",
diff: [ diff: [
{
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{ {
property: "name", property: "name",
previousValue: "joe", previousValue: "joe",
@ -446,6 +463,12 @@ describe("getObjectDiff", () => {
currentValue: ["football", "golf"], currentValue: ["football", "golf"],
status: "equal", status: "equal",
}, },
{
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{ {
property: "nickname", property: "nickname",
previousValue: undefined, previousValue: undefined,
@ -455,10 +478,10 @@ describe("getObjectDiff", () => {
], ],
}, },
{ {
property: "type", property: "country",
previousValue: "sport", previousValue: undefined,
currentValue: undefined, currentValue: "us",
status: "deleted", status: "added",
}, },
], ],
}); });
@ -536,10 +559,10 @@ describe("getObjectDiff", () => {
status: "updated", status: "updated",
diff: [ diff: [
{ {
property: "country", property: "type",
previousValue: undefined, previousValue: "sport",
currentValue: "us", currentValue: undefined,
status: "added", status: "deleted",
}, },
{ {
property: "user", property: "user",
@ -572,10 +595,10 @@ describe("getObjectDiff", () => {
], ],
}, },
{ {
property: "type", property: "country",
previousValue: "sport", previousValue: undefined,
currentValue: undefined, currentValue: "us",
status: "deleted", status: "added",
}, },
], ],
}); });
@ -822,26 +845,6 @@ describe("getObjectDiff", () => {
diff: [], 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", () => { it("returns all values if their status match the required statuses", () => {
expect( expect(
getObjectDiff( getObjectDiff(

Loading…
Cancel
Save