Superdiff provides a complete and readable diff for both arrays and objects. Plus, it supports stream and file inputs for handling large datasets efficiently, is battle-tested, has zero dependencies, and is super fast.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
5.7 KiB

2 years ago
import {
ObjectData,
ObjectDiff,
DiffStatus,
STATUS,
Subproperties,
} from "./model";
import { isObject, isEqual } from "./utils";
2 years ago
function getObjectStatus(diff: ObjectDiff["diff"]): DiffStatus {
return diff.some((property) => property.status !== STATUS.EQUAL)
? STATUS.UPDATED
: STATUS.EQUAL;
}
2 years ago
function formatSingleObjectDiff(
data: ObjectData,
status: DiffStatus
): ObjectDiff {
if (!data) {
return {
type: "object",
status: STATUS.isEqual,
diff: [],
};
}
2 years ago
const diff: ObjectDiff["diff"] = [];
Object.entries(data).forEach(([property, value]) => {
if (isObject(value)) {
2 years ago
const subPropertiesDiff: Subproperties[] = [];
Object.entries(value).forEach(([subProperty, subValue]) => {
subPropertiesDiff.push({
name: subProperty,
previousValue: status === STATUS.ADDED ? undefined : subValue,
currentValue: status === STATUS.ADDED ? subValue : undefined,
status,
});
});
return diff.push({
property: property,
previousValue: status === STATUS.ADDED ? undefined : data[property],
currentValue: status === STATUS.ADDED ? value : undefined,
status,
subPropertiesDiff,
});
}
return diff.push({
property,
previousValue: status === STATUS.ADDED ? undefined : data[property],
currentValue: status === STATUS.ADDED ? value : undefined,
status,
});
});
return {
type: "object",
status,
2 years ago
diff,
};
}
function getPreviousMatch(
previousValue: any | undefined,
nextSubProperty: any
): any | undefined {
if (!previousValue) {
return undefined;
}
const previousMatch = Object.entries(previousValue).find(([subPreviousKey]) =>
isEqual(subPreviousKey, nextSubProperty)
);
return previousMatch ? previousMatch[1] : undefined;
}
function getValueStatus(previousValue: any, nextValue: any): DiffStatus {
if (isEqual(previousValue, nextValue)) {
return STATUS.EQUAL;
}
return STATUS.UPDATED;
}
function getPropertyStatus(subPropertiesDiff: Subproperties[]): DiffStatus {
return subPropertiesDiff.some((property) => property.status !== STATUS.EQUAL)
? STATUS.UPDATED
: STATUS.EQUAL;
}
function getDeletedProperties(
previousValue: any,
nextValue: any,
nextProperty: string
) {
if (!previousValue) return undefined;
const previousMatch = previousValue[nextProperty];
if (!previousMatch) return undefined;
const nextMatch = nextValue[nextProperty];
const nextKeys = isObject(nextMatch) ? Object.keys(nextMatch) : [];
const prevKeys = isObject(previousMatch) ? Object.keys(previousMatch) : [];
const deletedKeys = prevKeys.filter(
(previousKey) => !nextKeys.includes(previousKey)
);
const result = deletedKeys.map((deletedKey) => ({
property: deletedKey,
value: previousMatch[deletedKey],
}));
if (result.length > 0) return result;
return undefined;
}
function getSubPropertiesDiff(
previousValue: Record<string, any> | undefined,
nextValue: Record<string, any>
): Subproperties[] {
const subPropertiesDiff: Subproperties[] = [];
let subDiff: Subproperties[];
Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => {
const previousMatch = getPreviousMatch(previousValue, nextSubProperty);
if (!!!previousMatch && !!nextSubProperty) {
return subPropertiesDiff.push({
name: nextSubProperty,
previousValue: undefined,
currentValue: nextSubValue,
status: STATUS.ADDED,
});
}
if (isObject(nextSubValue)) {
const data: Subproperties[] = getSubPropertiesDiff(
previousMatch,
nextSubValue
);
if (data && data.length > 0) {
subDiff = data;
}
const deletedProperties = getDeletedProperties(
previousValue,
nextValue,
nextSubProperty
);
if (deletedProperties) {
deletedProperties.forEach((deletedProperty) => {
const deletedData = {
name: deletedProperty.property,
previousValue: deletedProperty.value,
currentValue: undefined,
status: STATUS.DELETED,
};
if (subDiff) {
subDiff.push(deletedData);
} else {
subDiff = [deletedData];
}
});
}
}
if (previousMatch) {
subPropertiesDiff.push({
name: nextSubProperty,
previousValue: previousMatch,
currentValue: nextSubValue,
status: getValueStatus(previousMatch, nextSubValue),
...(!!subDiff && { subDiff }),
});
}
});
return subPropertiesDiff;
}
2 years ago
export function getObjectDiff(
prevData: ObjectData,
nextData: ObjectData
): ObjectDiff {
if (!prevData && !nextData) {
return {
type: "object",
status: STATUS.EQUAL,
2 years ago
diff: [],
};
}
if (!prevData) {
return formatSingleObjectDiff(nextData, STATUS.ADDED);
}
if (!nextData) {
return formatSingleObjectDiff(prevData, STATUS.DELETED);
}
const diff: ObjectDiff["diff"] = [];
Object.entries(nextData).forEach(([nextProperty, nextValue]) => {
const previousValue = prevData[nextProperty];
if (isObject(nextValue)) {
const subPropertiesDiff: Subproperties[] = getSubPropertiesDiff(
previousValue,
nextValue
);
2 years ago
return diff.push({
property: nextProperty,
previousValue,
currentValue: nextValue,
status: getPropertyStatus(subPropertiesDiff),
2 years ago
subPropertiesDiff,
});
}
return diff.push({
property: nextProperty,
previousValue,
currentValue: nextValue,
status: getValueStatus(previousValue, nextValue),
2 years ago
});
});
return {
type: "object",
status: getObjectStatus(diff),
2 years ago
diff,
};
}