commit
7c5ecf0551
13 changed files with 3234 additions and 0 deletions
@ -0,0 +1,15 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,7 @@
|
||||
import { getListDiff } from "../src/list-diff"; |
||||
|
||||
describe("getListDiff", () => { |
||||
it("", () => { |
||||
expect(getListDiff(null, null)).toStrictEqual(null); |
||||
}); |
||||
}); |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
import { getObjectDiff } from "../src/object-diff"; |
||||
|
||||
describe("getObjectDiff", () => { |
||||
it("", () => { |
||||
expect(getObjectDiff(null, null)).toStrictEqual(null); |
||||
}); |
||||
}); |
@ -0,0 +1,10 @@
@@ -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