commit
7c5ecf0551
13 changed files with 3234 additions and 0 deletions
@ -0,0 +1,15 @@ |
|||||||
|
{ |
||||||
|
"name": "data-diff", |
||||||
|
"version": "1.0.0", |
||||||
|
"main": "index.js", |
||||||
|
"license": "MIT", |
||||||
|
"devDependencies": { |
||||||
|
"@babel/preset-env": "^7.20.2", |
||||||
|
"@types/jest": "^29.2.4", |
||||||
|
"jest": "^29.3.1", |
||||||
|
"mkdirp": "^1.0.4", |
||||||
|
"rimraf": "^3.0.2", |
||||||
|
"ts-jest": "^29.0.3", |
||||||
|
"typescript": "^4.9.4" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,82 @@ |
|||||||
|
import { STATUS, ListDiff, ListData } from "./model"; |
||||||
|
import { isEqual } from "./utils"; |
||||||
|
|
||||||
|
function formatSingleListDiff( |
||||||
|
listData: ListData, |
||||||
|
status: "added" | "removed" |
||||||
|
): ListDiff { |
||||||
|
return { |
||||||
|
type: "list", |
||||||
|
diff: listData.map((data) => ({ value: data, status })), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export const getListDiff = ( |
||||||
|
prevList: ListData[] | undefined | null, |
||||||
|
nextList: ListData[] | undefined | null |
||||||
|
): ListDiff => { |
||||||
|
if (!prevList && !nextList) { |
||||||
|
return { |
||||||
|
type: "list", |
||||||
|
diff: [], |
||||||
|
}; |
||||||
|
} |
||||||
|
if (!prevList) { |
||||||
|
return formatSingleListDiff(nextList, "added"); |
||||||
|
} |
||||||
|
if (!nextList) { |
||||||
|
return formatSingleListDiff(prevList, "removed"); |
||||||
|
} |
||||||
|
const diff: ListDiff["diff"] = []; |
||||||
|
nextList.forEach((nextValue, i) => { |
||||||
|
const prevIndex = prevList.findIndex((prevValue) => |
||||||
|
isEqual(prevValue, nextValue) |
||||||
|
); |
||||||
|
const indexDiff = prevIndex === -1 ? null : i - prevIndex; |
||||||
|
if (indexDiff === 0) { |
||||||
|
return diff.push({ |
||||||
|
value: nextValue, |
||||||
|
prevIndex, |
||||||
|
newIndex: i, |
||||||
|
indexDiff, |
||||||
|
status: STATUS.EQUAL, |
||||||
|
}); |
||||||
|
} |
||||||
|
if (prevIndex === -1) { |
||||||
|
return diff.push({ |
||||||
|
value: nextValue, |
||||||
|
prevIndex: null, |
||||||
|
newIndex: i, |
||||||
|
indexDiff, |
||||||
|
status: STATUS.ADDED, |
||||||
|
}); |
||||||
|
} |
||||||
|
return diff.push({ |
||||||
|
value: nextValue, |
||||||
|
prevIndex, |
||||||
|
newIndex: i, |
||||||
|
indexDiff, |
||||||
|
status: STATUS.MOVED, |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
prevList.forEach((prevValue, i) => { |
||||||
|
if (!nextList.some((nextValue) => isEqual(nextValue, prevValue))) { |
||||||
|
return diff.splice(i, 0, { |
||||||
|
value: prevValue, |
||||||
|
prevIndex: i, |
||||||
|
newIndex: null, |
||||||
|
indexDiff: null, |
||||||
|
status: STATUS.DELETED, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
return { |
||||||
|
type: "list", |
||||||
|
diff, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export function hasListChanged(listDiff: ListDiff): boolean { |
||||||
|
return listDiff.diff.some((d) => d.status !== STATUS.EQUAL); |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
export const STATUS: Record<string, DiffStatus> = { |
||||||
|
ADDED: "added", |
||||||
|
EQUAL: "equal", |
||||||
|
MOVED: "moved", |
||||||
|
DELETED: "deleted", |
||||||
|
UPDATED: "updated", |
||||||
|
}; |
||||||
|
|
||||||
|
export type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; |
||||||
|
export type ObjectData = Record<string, any> | undefined | null; |
||||||
|
export type ListData = any; |
||||||
|
|
||||||
|
export type ListDiff = { |
||||||
|
type: "list"; |
||||||
|
diff: { |
||||||
|
value: ListData; |
||||||
|
prevIndex: number | null; |
||||||
|
newIndex: number | null; |
||||||
|
indexDiff: number | null; |
||||||
|
status: DiffStatus; |
||||||
|
}[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export type Subproperties = { |
||||||
|
name: string; |
||||||
|
previousValue: any; |
||||||
|
currentValue: any; |
||||||
|
status: DiffStatus; |
||||||
|
}; |
||||||
|
|
||||||
|
export type ObjectDiff = { |
||||||
|
type: "object"; |
||||||
|
diff: { |
||||||
|
property: string; |
||||||
|
previousValue: any; |
||||||
|
currentValue: any; |
||||||
|
status: DiffStatus; |
||||||
|
subPropertiesDiff?: Subproperties[]; |
||||||
|
}[]; |
||||||
|
}; |
||||||
|
|
||||||
|
export type DataDiff = ListDiff | ObjectDiff; |
@ -0,0 +1,113 @@ |
|||||||
|
import { |
||||||
|
ObjectData, |
||||||
|
ObjectDiff, |
||||||
|
DiffStatus, |
||||||
|
STATUS, |
||||||
|
Subproperties, |
||||||
|
} from "./model"; |
||||||
|
import { hasNestedValues, isEqual } from "./utils"; |
||||||
|
|
||||||
|
function formatSingleObjectDiff( |
||||||
|
data: ObjectData, |
||||||
|
status: DiffStatus |
||||||
|
): ObjectDiff { |
||||||
|
const diff: ObjectDiff["diff"] = []; |
||||||
|
Object.entries(data).forEach(([property, value]) => { |
||||||
|
if (hasNestedValues(value)) { |
||||||
|
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", |
||||||
|
diff, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
export function getObjectDiff( |
||||||
|
prevData: ObjectData, |
||||||
|
nextData: ObjectData |
||||||
|
): ObjectDiff { |
||||||
|
if (!prevData && !nextData) { |
||||||
|
return { |
||||||
|
type: "object", |
||||||
|
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 (hasNestedValues(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 _status = subPropertiesDiff.some( |
||||||
|
(property) => property.status !== STATUS.EQUAL |
||||||
|
) |
||||||
|
? STATUS.UPDATED |
||||||
|
: STATUS.EQUAL; |
||||||
|
return diff.push({ |
||||||
|
property: nextProperty, |
||||||
|
previousValue, |
||||||
|
currentValue: nextValue, |
||||||
|
status: _status, |
||||||
|
subPropertiesDiff, |
||||||
|
}); |
||||||
|
} |
||||||
|
return diff.push({ |
||||||
|
property: nextProperty, |
||||||
|
previousValue, |
||||||
|
currentValue: nextValue, |
||||||
|
status: previousValue === nextValue ? STATUS.EQUAL : STATUS.UPDATED, |
||||||
|
}); |
||||||
|
}); |
||||||
|
return { |
||||||
|
type: "object", |
||||||
|
diff, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
export function isEqual(a: any, b: any): boolean { |
||||||
|
if (typeof a !== typeof b) return true; |
||||||
|
if (Array.isArray(a)) { |
||||||
|
return a.toString() === b.toString(); |
||||||
|
} |
||||||
|
if (typeof a === "object") { |
||||||
|
return JSON.stringify(a) === JSON.stringify(b); |
||||||
|
} |
||||||
|
return a === b; |
||||||
|
} |
||||||
|
|
||||||
|
export function hasNestedValues(value: any): value is Record<string, any> { |
||||||
|
return typeof value === "object" && !Array.isArray(value); |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
import { getListDiff } from "../src/list-diff"; |
||||||
|
|
||||||
|
describe("getListDiff", () => { |
||||||
|
it("", () => { |
||||||
|
expect(getListDiff(null, null)).toStrictEqual(null); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,7 @@ |
|||||||
|
import { getObjectDiff } from "../src/object-diff"; |
||||||
|
|
||||||
|
describe("getObjectDiff", () => { |
||||||
|
it("", () => { |
||||||
|
expect(getObjectDiff(null, null)).toStrictEqual(null); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,10 @@ |
|||||||
|
import { isEqual, hasNestedValues } from "../src/utils"; |
||||||
|
|
||||||
|
describe("isEqual", () => { |
||||||
|
it("", () => { |
||||||
|
expect(isEqual(null, null)).toStrictEqual(null); |
||||||
|
}); |
||||||
|
it("", () => { |
||||||
|
expect(hasNestedValues(null)).toStrictEqual(null); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue