Browse Source

feat: simplify output, clean readme

BREAKING CHANGE: subPropertiesDiff and subDiff have been removed from the object diff, there is now a single diff recursive key for more simplicity
pull/25/head
Antoine Lanoe 7 months ago
parent
commit
6f3f492584
  1. 1
      .eslintcache
  2. 1
      .gitignore
  3. 141
      README.md
  4. 6
      eslint.config.mjs
  5. 2
      package.json
  6. 3
      src/index.ts
  7. 28
      src/list-diff.ts
  8. 93
      src/model.ts
  9. 35
      src/models/list.ts
  10. 41
      src/models/object.ts
  11. 3
      src/models/utils.ts
  12. 171
      src/object-diff.ts
  13. 18
      src/utils.ts
  14. 7
      test/list-diff.test.ts
  15. 58
      test/object-diff.test.ts

1
.eslintcache

@ -1 +0,0 @@
[{"/Users/antoine/Desktop/superdiff/eslint.config.mjs":"1","/Users/antoine/Desktop/superdiff/index.ts":"2","/Users/antoine/Desktop/superdiff/tsup.config.ts":"3","/Users/antoine/Desktop/superdiff/src/index.ts":"4","/Users/antoine/Desktop/superdiff/src/list-diff.ts":"5","/Users/antoine/Desktop/superdiff/src/model.ts":"6","/Users/antoine/Desktop/superdiff/src/object-diff.ts":"7","/Users/antoine/Desktop/superdiff/src/utils.ts":"8","/Users/antoine/Desktop/superdiff/test/list-diff.test.ts":"9","/Users/antoine/Desktop/superdiff/test/object-diff.test.ts":"10","/Users/antoine/Desktop/superdiff/test/utils.test.ts":"11"},{"size":426,"mtime":1727602294779,"results":"12","hashOfConfig":"13"},{"size":0,"mtime":1727601340027,"results":"14","hashOfConfig":"15"},{"size":222,"mtime":1727601767512,"results":"16","hashOfConfig":"15"},{"size":160,"mtime":1727601340028,"results":"17","hashOfConfig":"15"},{"size":4697,"mtime":1727601897826,"results":"18","hashOfConfig":"15"},{"size":1903,"mtime":1727601897832,"results":"19","hashOfConfig":"15"},{"size":9363,"mtime":1727602282176,"results":"20","hashOfConfig":"15"},{"size":1150,"mtime":1727601897855,"results":"21","hashOfConfig":"15"},{"size":18523,"mtime":1727601897889,"results":"22","hashOfConfig":"15"},{"size":23198,"mtime":1727601897918,"results":"23","hashOfConfig":"15"},{"size":3560,"mtime":1727601897925,"results":"24","hashOfConfig":"15"},{"filePath":"25","messages":"26","suppressedMessages":"27","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"13i3tau",{"filePath":"28","messages":"29","suppressedMessages":"30","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"a3qfx6",{"filePath":"31","messages":"32","suppressedMessages":"33","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"34","messages":"35","suppressedMessages":"36","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"37","messages":"38","suppressedMessages":"39","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"40","messages":"41","suppressedMessages":"42","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"43","messages":"44","suppressedMessages":"45","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"46","messages":"47","suppressedMessages":"48","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"49","messages":"50","suppressedMessages":"51","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"52","messages":"53","suppressedMessages":"54","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"55","messages":"56","suppressedMessages":"57","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/antoine/Desktop/superdiff/eslint.config.mjs",[],[],"/Users/antoine/Desktop/superdiff/index.ts",[],[],"/Users/antoine/Desktop/superdiff/tsup.config.ts",[],[],"/Users/antoine/Desktop/superdiff/src/index.ts",[],[],"/Users/antoine/Desktop/superdiff/src/list-diff.ts",[],[],"/Users/antoine/Desktop/superdiff/src/model.ts",[],[],"/Users/antoine/Desktop/superdiff/src/object-diff.ts",[],[],"/Users/antoine/Desktop/superdiff/src/utils.ts",[],[],"/Users/antoine/Desktop/superdiff/test/list-diff.test.ts",[],[],"/Users/antoine/Desktop/superdiff/test/object-diff.test.ts",[],[],"/Users/antoine/Desktop/superdiff/test/utils.test.ts",[],[]]

1
.gitignore vendored

@ -1,2 +1,3 @@
/node_modules /node_modules
dist dist
.eslintcache

141
README.md

@ -1,20 +1,27 @@
<img width="722" alt="superdiff-logo" src="https://user-images.githubusercontent.com/43271780/209532864-24d7449e-1185-4810-9423-be5df1fe877f.png"> <img width="722" alt="superdiff-logo" src="https://user-images.githubusercontent.com/43271780/209532864-24d7449e-1185-4810-9423-be5df1fe877f.png">
# SUPERDIFF
This library compares two arrays or objects and returns a full diff of their differences.
[![CI](https://github.com/DoneDeal0/superdiff/actions/workflows/ci.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/ci.yml) [![CI](https://github.com/DoneDeal0/superdiff/actions/workflows/ci.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/ci.yml)
[![CD](https://github.com/DoneDeal0/superdiff/actions/workflows/cd.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/cd.yml) [![CD](https://github.com/DoneDeal0/superdiff/actions/workflows/cd.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/cd.yml)
![NPM Downloads](https://img.shields.io/npm/dy/%40donedeal0%2Fsuperdiff?logo=npm) ![NPM Downloads](https://img.shields.io/npm/dy/%40donedeal0%2Fsuperdiff?logo=npm)
![GitHub Tag](https://img.shields.io/github/v/tag/DoneDeal0/superdiff?label=latest%20release) ![GitHub Tag](https://img.shields.io/github/v/tag/DoneDeal0/superdiff?label=latest%20release)
<hr/>
# WHAT IS IT?
This library compares two arrays or objects and returns a full diff of their differences.
<hr/>
## WHY YOU SHOULD USE THIS LIBRARY ## WHY YOU SHOULD USE THIS LIBRARY
All other existing solutions return a strange diff format that often requires additional parsing. They are also limited to object comparison. 👎 All other existing solutions return a strange diff format that often requires additional parsing. They are also limited to object comparison.
**Superdiff** gives you a complete diff for both array <u>and</u> objects in a very readable format. Last but not least, it's battle-tested and super fast. Import. Enjoy. 👍 **Superdiff** gives you a complete diff for both array <u>and</u> objects in a very readable format. Last but not least, it's battle-tested and super fast. Import. Enjoy. 👍
<hr/>
## DONORS ## DONORS
I am grateful to the generous donors of **Superdiff**! I am grateful to the generous donors of **Superdiff**!
@ -27,111 +34,7 @@ I am grateful to the generous donors of **Superdiff**!
</div> </div>
## DIFF FORMAT COMPARISON <hr/>
Let's compare the diff format of **Superdiff** and **Deep-diff**, the most popular diff lib on npm:
input:
```diff
const objectA = {
id: 54,
user: {
name: "joe",
- member: true,
- hobbies: ["golf", "football"],
age: 66,
},
}
const objectB = {
id: 54,
user: {
name: "joe",
+ member: false,
+ hobbies: ["golf", "chess"],
age: 66,
},
}
```
**Deep-Diff** output:
```js
[
{
kind: "E",
path: ["user", "member"],
lhs: true,
rhs: false,
},
{
kind: "E",
path: ["user", "hobbies", 1],
lhs: "football",
rhs: "chess",
},
];
```
**SuperDiff** output:
```diff
{
type: "object",
+ status: "updated",
diff: [
{
property: "id",
previousValue: 54,
currentValue: 54,
status: "equal",
},
{
property: "user",
previousValue: {
name: "joe",
member: true,
hobbies: ["golf", "football"],
age: 66,
},
currentValue: {
name: "joe",
member: false,
hobbies: ["golf", "chess"],
age: 66,
},
+ status: "updated",
subPropertiesDiff: [
{
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
+ {
+ property: "member",
+ previousValue: true,
+ currentValue: false,
+ status: "updated",
+ },
+ {
+ property: "hobbies",
+ previousValue: ["golf", "football"],
+ currentValue: ["golf", "chess"],
+ status: "updated",
+ },
{
property: "age",
previousValue: 66,
currentValue: 66,
status: "equal",
},
],
},
],
}
```
## FEATURES ## FEATURES
@ -158,17 +61,17 @@ type ObjectDiff = {
status: "added" | "deleted" | "equal" | "updated"; status: "added" | "deleted" | "equal" | "updated";
diff: { diff: {
property: string; property: string;
previousValue: any; previousValue: unknown;
currentValue: any; currentValue: unknow;
status: "added" | "deleted" | "equal" | "updated"; status: "added" | "deleted" | "equal" | "updated";
// only appears if some subproperties have been added/deleted/updated // only appears if some subproperties have been added/deleted/updated
subPropertiesDiff?: { diff?: {
property: string; property: string;
previousValue: any; previousValue: unknown;
currentValue: any; currentValue: unknown;
status: "added" | "deleted" | "equal" | "updated"; status: "added" | "deleted" | "equal" | "updated";
// subDiff is a recursive diff in case of nested subproperties // recursive diff in case of subproperties
subDiff?: SubProperties[]; diff?: SubDiff[];
}[]; }[];
}[]; }[];
}; };
@ -217,7 +120,7 @@ type ListDiff = {
type: "list"; type: "list";
status: "added" | "deleted" | "equal" | "moved" | "updated"; status: "added" | "deleted" | "equal" | "moved" | "updated";
diff: { diff: {
value: any; value: unknown;
prevIndex: number | null; prevIndex: number | null;
newIndex: number | null; newIndex: number | null;
indexDiff: number | null; indexDiff: number | null;
@ -270,6 +173,8 @@ import { isObject } from "@donedeal0/superdiff";
Tests whether a value is an object. Tests whether a value is an object.
<hr/>
## EXAMPLES ## EXAMPLES
### getListDiff() ### getListDiff()
@ -384,7 +289,7 @@ output
age: 66, age: 66,
}, },
+ status: "updated", + status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "name", property: "name",
previousValue: "joe", previousValue: "joe",

6
eslint.config.mjs

@ -7,10 +7,4 @@ export default [
{ settings: { react: { version: "detect" } } }, { settings: { react: { version: "detect" } } },
pluginJs.configs.recommended, pluginJs.configs.recommended,
...tseslint.configs.recommended, ...tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off"
},
},
]; ];

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "@donedeal0/superdiff", "name": "@donedeal0/superdiff",
"version": "1.1.3", "version": "2.0.0",
"description": "SuperDiff checks the changes between two objects or arrays. It returns a complete diff with relevant information for each property or piece of data", "description": "SuperDiff checks the changes between two objects or arrays. It returns a complete diff with relevant information for each property or piece of data",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.mjs", "module": "dist/index.mjs",

3
src/index.ts

@ -1,4 +1,5 @@
export { getObjectDiff } from "./object-diff"; export { getObjectDiff } from "./object-diff";
export { getListDiff } from "./list-diff"; export { getListDiff } from "./list-diff";
export { isEqual, isObject } from "./utils"; export { isEqual, isObject } from "./utils";
export * from "./model"; export * from "./models/list";
export * from "./models/object";

28
src/list-diff.ts

@ -1,17 +1,22 @@
import { LIST_STATUS, ListDiff, ListDiffStatus, ListOptions } from "./model"; import {
DEFAULT_LIST_DIFF_OPTIONS,
LIST_STATUS,
ListDiff,
ListDiffOptions,
} from "./models/list";
import { isEqual, isObject } from "./utils"; import { isEqual, isObject } from "./utils";
function getLeanDiff( function getLeanDiff(
diff: ListDiff["diff"], diff: ListDiff["diff"],
showOnly = [] as ListOptions["showOnly"], showOnly = [] as ListDiffOptions["showOnly"],
): ListDiff["diff"] { ): ListDiff["diff"] {
return diff.filter((value) => showOnly?.includes(value.status)); return diff.filter((value) => showOnly?.includes(value.status));
} }
function formatSingleListDiff<T>( function formatSingleListDiff<T>(
listData: T[], listData: T[],
status: ListDiffStatus, status: LIST_STATUS,
options: ListOptions = { showOnly: [] }, options: ListDiffOptions = { showOnly: [] },
): ListDiff { ): ListDiff {
const diff = listData.map((data, i) => ({ const diff = listData.map((data, i) => ({
value: data, value: data,
@ -34,16 +39,16 @@ function formatSingleListDiff<T>(
}; };
} }
function getListStatus(listDiff: ListDiff["diff"]): ListDiffStatus { function getListStatus(listDiff: ListDiff["diff"]): LIST_STATUS {
return listDiff.some((value) => value.status !== LIST_STATUS.EQUAL) return listDiff.some((value) => value.status !== LIST_STATUS.EQUAL)
? LIST_STATUS.UPDATED ? LIST_STATUS.UPDATED
: LIST_STATUS.EQUAL; : LIST_STATUS.EQUAL;
} }
function isReferencedObject( function isReferencedObject(
value: any, value: unknown,
referenceProperty: ListOptions["referenceProperty"], referenceProperty: ListDiffOptions["referenceProperty"],
): value is Record<string, any> { ): value is Record<string, unknown> {
if (isObject(value) && !!referenceProperty) { if (isObject(value) && !!referenceProperty) {
return Object.hasOwn(value, referenceProperty); return Object.hasOwn(value, referenceProperty);
} }
@ -62,12 +67,7 @@ function isReferencedObject(
export const getListDiff = <T>( export const getListDiff = <T>(
prevList: T[] | undefined | null, prevList: T[] | undefined | null,
nextList: T[] | undefined | null, nextList: T[] | undefined | null,
options: ListOptions = { options: ListDiffOptions = DEFAULT_LIST_DIFF_OPTIONS,
showOnly: [],
referenceProperty: undefined,
considerMoveAsUpdate: false,
ignoreArrayOrder: false,
},
): ListDiff => { ): ListDiff => {
if (!prevList && !nextList) { if (!prevList && !nextList) {
return { return {

93
src/model.ts

@ -1,93 +0,0 @@
export const STATUS: Record<string, ObjectDiffStatus> = {
ADDED: "added",
EQUAL: "equal",
DELETED: "deleted",
UPDATED: "updated",
};
export const LIST_STATUS: Record<string, ListDiffStatus> = {
...STATUS,
MOVED: "moved",
};
export const GRANULARITY: Record<string, "basic" | "deep"> = {
BASIC: "basic",
DEEP: "deep",
};
export type ListDiffStatus =
| "added"
| "equal"
| "moved"
| "deleted"
| "updated";
export type ObjectDiffStatus = "added" | "equal" | "deleted" | "updated";
export type ObjectData = Record<string, any> | undefined | null;
export type ListData = any;
export type ObjectStatusTuple = readonly [
"added",
"equal",
"deleted",
"updated",
];
export type ListStatusTuple = readonly [
"added",
"equal",
"deleted",
"moved",
"updated",
];
export type isEqualOptions = {
ignoreArrayOrder?: boolean;
};
export type ObjectOptions = {
ignoreArrayOrder?: boolean;
showOnly?: {
statuses: Array<ObjectStatusTuple[number]>;
granularity?: (typeof GRANULARITY)[keyof typeof GRANULARITY];
};
};
export type ListOptions = {
showOnly?: Array<ListStatusTuple[number]>;
referenceProperty?: string;
considerMoveAsUpdate?: boolean;
ignoreArrayOrder?: boolean;
};
export type ListDiff = {
type: "list";
status: ListDiffStatus;
diff: {
value: ListData;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: ListDiffStatus;
}[];
};
export type SubProperties = {
property: string;
previousValue: any;
currentValue: any;
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
};
export type ObjectDiff = {
type: "object";
status: ObjectDiffStatus;
diff: {
property: string;
previousValue: any;
currentValue: any;
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
}[];
};
export type DataDiff = ListDiff | ObjectDiff;

35
src/models/list.ts

@ -0,0 +1,35 @@
export enum LIST_STATUS {
ADDED = "added",
EQUAL = "equal",
DELETED = "deleted",
UPDATED = "updated",
MOVED = "moved",
}
export type ListData = unknown;
export type ListDiffOptions = {
showOnly?: `${LIST_STATUS}`[];
referenceProperty?: string;
considerMoveAsUpdate?: boolean;
ignoreArrayOrder?: boolean;
};
export const DEFAULT_LIST_DIFF_OPTIONS = {
showOnly: [],
referenceProperty: undefined,
considerMoveAsUpdate: false,
ignoreArrayOrder: false,
};
export type ListDiff = {
type: "list";
status: LIST_STATUS;
diff: {
value: ListData;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: LIST_STATUS;
}[];
};

41
src/models/object.ts

@ -0,0 +1,41 @@
export enum OBJECT_STATUS {
ADDED = "added",
EQUAL = "equal",
DELETED = "deleted",
UPDATED = "updated",
}
export enum GRANULARITY {
BASIC = "basic",
DEEP = "deep",
}
export type ObjectData = Record<string, unknown> | undefined | null;
export type ObjectDiffOptions = {
ignoreArrayOrder?: boolean;
showOnly?: {
statuses: `${OBJECT_STATUS}`[];
granularity?: `${GRANULARITY}`;
};
};
export const DEFAULT_OBJECT_DIFF_OPTIONS = {
ignoreArrayOrder: false,
showOnly: { statuses: [], granularity: GRANULARITY.BASIC },
};
/** recursive diff in case of subproperties */
export type Diff = {
property: string;
previousValue: unknown;
currentValue: unknown;
status: OBJECT_STATUS;
diff?: Diff[];
};
export type ObjectDiff = {
type: "object";
status: OBJECT_STATUS;
diff: Diff[];
};

3
src/models/utils.ts

@ -0,0 +1,3 @@
export type isEqualOptions = {
ignoreArrayOrder?: boolean;
};

171
src/object-diff.ts

@ -1,42 +1,25 @@
import { import {
GRANULARITY, GRANULARITY,
STATUS, OBJECT_STATUS,
ObjectData, ObjectData,
ObjectDiff, ObjectDiff,
ObjectDiffStatus, ObjectDiffOptions,
ObjectOptions, Diff,
SubProperties, DEFAULT_OBJECT_DIFF_OPTIONS,
} from "./model"; } from "./models/object";
import { isEqual, isObject } from "./utils"; import { isEqual, isObject } from "./utils";
function getLeanDiff( function getLeanDiff(
diff: ObjectDiff["diff"], diff: ObjectDiff["diff"],
showOnly: ObjectOptions["showOnly"] = { showOnly: ObjectDiffOptions["showOnly"] = DEFAULT_OBJECT_DIFF_OPTIONS.showOnly,
statuses: [],
granularity: GRANULARITY.BASIC,
},
): ObjectDiff["diff"] { ): ObjectDiff["diff"] {
const { statuses, granularity } = showOnly; const { statuses, granularity } = showOnly;
return diff.reduce( return diff.reduce(
(acc, value) => { (acc, value) => {
if (granularity === GRANULARITY.DEEP && value.subPropertiesDiff) { if (granularity === GRANULARITY.DEEP && value.diff) {
const cleanSubPropertiesDiff = getLeanDiff( const leanDiff = getLeanDiff(value.diff, showOnly);
value.subPropertiesDiff, if (leanDiff.length > 0) {
showOnly, return [...acc, { ...value, diff: leanDiff }];
);
if (cleanSubPropertiesDiff.length > 0) {
return [
...acc,
{ ...value, subPropertiesDiff: cleanSubPropertiesDiff },
];
}
}
// @ts-ignore
if (granularity === GRANULARITY.DEEP && value.subDiff) {
// @ts-ignore
const cleanSubDiff = getLeanDiff(value.subDiff, showOnly);
if (cleanSubDiff.length > 0) {
return [...acc, { ...value, subDiff: cleanSubDiff }];
} }
} }
if (statuses.includes(value.status)) { if (statuses.includes(value.status)) {
@ -48,51 +31,50 @@ function getLeanDiff(
); );
} }
function getObjectStatus(diff: ObjectDiff["diff"]): ObjectDiffStatus { function getObjectStatus(diff: ObjectDiff["diff"]): OBJECT_STATUS {
return diff.some((property) => property.status !== STATUS.EQUAL) return diff.some((property) => property.status !== OBJECT_STATUS.EQUAL)
? STATUS.UPDATED ? OBJECT_STATUS.UPDATED
: STATUS.EQUAL; : OBJECT_STATUS.EQUAL;
} }
function formatSingleObjectDiff( function formatSingleObjectDiff(
data: ObjectData, data: ObjectData,
status: ObjectDiffStatus, status: OBJECT_STATUS,
options: ObjectOptions = { options: ObjectDiffOptions = DEFAULT_OBJECT_DIFF_OPTIONS,
ignoreArrayOrder: false,
showOnly: { statuses: [], granularity: GRANULARITY.BASIC },
},
): ObjectDiff { ): ObjectDiff {
if (!data) { if (!data) {
return { return {
type: "object", type: "object",
status: STATUS.EQUAL, status: OBJECT_STATUS.EQUAL,
diff: [], diff: [],
}; };
} }
const diff: ObjectDiff["diff"] = []; const diff: ObjectDiff["diff"] = [];
Object.entries(data).forEach(([property, value]) => { Object.entries(data).forEach(([property, value]) => {
if (isObject(value)) { if (isObject(value)) {
const subPropertiesDiff: SubProperties[] = []; const subPropertiesDiff: Diff[] = [];
Object.entries(value).forEach(([subProperty, subValue]) => { Object.entries(value).forEach(([subProperty, subValue]) => {
subPropertiesDiff.push({ subPropertiesDiff.push({
property: subProperty, property: subProperty,
previousValue: status === STATUS.ADDED ? undefined : subValue, previousValue: status === OBJECT_STATUS.ADDED ? undefined : subValue,
currentValue: status === STATUS.ADDED ? subValue : undefined, currentValue: status === OBJECT_STATUS.ADDED ? subValue : undefined,
status, status,
}); });
}); });
return diff.push({ return diff.push({
property: property, property,
previousValue: status === STATUS.ADDED ? undefined : data[property], previousValue:
currentValue: status === STATUS.ADDED ? value : undefined, status === OBJECT_STATUS.ADDED ? undefined : data[property],
currentValue: status === OBJECT_STATUS.ADDED ? value : undefined,
status, status,
subPropertiesDiff, diff: subPropertiesDiff,
}); });
} }
return diff.push({ return diff.push({
property, property,
previousValue: status === STATUS.ADDED ? undefined : data[property], previousValue:
currentValue: status === STATUS.ADDED ? value : undefined, status === OBJECT_STATUS.ADDED ? undefined : data[property],
currentValue: status === OBJECT_STATUS.ADDED ? value : undefined,
status, status,
}); });
}); });
@ -111,10 +93,10 @@ function formatSingleObjectDiff(
} }
function getPreviousMatch( function getPreviousMatch(
previousValue: any | undefined, previousValue: unknown | undefined,
nextSubProperty: any, nextSubProperty: unknown,
options?: ObjectOptions, options?: ObjectDiffOptions,
): any | undefined { ): unknown | undefined {
if (!previousValue) { if (!previousValue) {
return undefined; return undefined;
} }
@ -125,28 +107,28 @@ function getPreviousMatch(
} }
function getValueStatus( function getValueStatus(
previousValue: any, previousValue: unknown,
nextValue: any, nextValue: unknown,
options?: ObjectOptions, options?: ObjectDiffOptions,
): ObjectDiffStatus { ): OBJECT_STATUS {
if (isEqual(previousValue, nextValue, options)) { if (isEqual(previousValue, nextValue, options)) {
return STATUS.EQUAL; return OBJECT_STATUS.EQUAL;
} }
return STATUS.UPDATED; return OBJECT_STATUS.UPDATED;
} }
function getPropertyStatus( function getPropertyStatus(subPropertiesDiff: Diff[]): OBJECT_STATUS {
subPropertiesDiff: SubProperties[], return subPropertiesDiff.some(
): ObjectDiffStatus { (property) => property.status !== OBJECT_STATUS.EQUAL,
return subPropertiesDiff.some((property) => property.status !== STATUS.EQUAL) )
? STATUS.UPDATED ? OBJECT_STATUS.UPDATED
: STATUS.EQUAL; : OBJECT_STATUS.EQUAL;
} }
function getDeletedProperties( function getDeletedProperties(
previousValue: Record<string, any> | undefined, previousValue: Record<string, unknown> | undefined,
nextValue: Record<string, any>, nextValue: Record<string, unknown>,
): { property: string; value: any }[] | undefined { ): { property: string; value: unknown }[] | undefined {
if (!previousValue) return undefined; if (!previousValue) return undefined;
const prevKeys = Object.keys(previousValue); const prevKeys = Object.keys(previousValue);
const nextKeys = Object.keys(nextValue); const nextKeys = Object.keys(nextValue);
@ -161,12 +143,12 @@ function getDeletedProperties(
} }
function getSubPropertiesDiff( function getSubPropertiesDiff(
previousValue: Record<string, any> | undefined, previousValue: Record<string, unknown> | undefined,
nextValue: Record<string, any>, nextValue: Record<string, unknown>,
options?: ObjectOptions, options?: ObjectDiffOptions,
): SubProperties[] { ): Diff[] {
const subPropertiesDiff: SubProperties[] = []; const subPropertiesDiff: Diff[] = [];
let subDiff: SubProperties[]; let subDiff: Diff[];
const deletedMainSubProperties = getDeletedProperties( const deletedMainSubProperties = getDeletedProperties(
previousValue, previousValue,
nextValue, nextValue,
@ -177,7 +159,7 @@ function getSubPropertiesDiff(
property: deletedProperty.property, property: deletedProperty.property,
previousValue: deletedProperty.value, previousValue: deletedProperty.value,
currentValue: undefined, currentValue: undefined,
status: STATUS.DELETED, status: OBJECT_STATUS.DELETED,
}); });
}); });
} }
@ -194,15 +176,15 @@ function getSubPropertiesDiff(
currentValue: nextSubValue, currentValue: nextSubValue,
status: status:
!previousValue || !(nextSubProperty in previousValue) !previousValue || !(nextSubProperty in previousValue)
? STATUS.ADDED ? OBJECT_STATUS.ADDED
: previousMatch === nextSubValue : previousMatch === nextSubValue
? STATUS.EQUAL ? OBJECT_STATUS.EQUAL
: STATUS.UPDATED, : OBJECT_STATUS.UPDATED,
}); });
} }
if (isObject(nextSubValue)) { if (isObject(nextSubValue)) {
const data: SubProperties[] = getSubPropertiesDiff( const data: Diff[] = getSubPropertiesDiff(
previousMatch, previousMatch as Record<string, unknown>,
nextSubValue, nextSubValue,
options, options,
); );
@ -216,7 +198,7 @@ function getSubPropertiesDiff(
previousValue: previousMatch, previousValue: previousMatch,
currentValue: nextSubValue, currentValue: nextSubValue,
status: getValueStatus(previousMatch, nextSubValue, options), status: getValueStatus(previousMatch, nextSubValue, options),
...(!!subDiff && { subDiff }), ...(!!subDiff && { diff: subDiff }),
}); });
} }
}); });
@ -225,9 +207,9 @@ function getSubPropertiesDiff(
/** /**
* Returns the diff between two objects * Returns the diff between two objects
* @param {Record<string, any>} prevData - The original object. * @param {ObjectData} prevData - The original object.
* @param {Record<string, any>} nextData - The new object. * @param {ObjectData} nextData - The new object.
* * @param {ListOptions} options - Options to refine your output. * * @param {ObjectOptions} options - Options to refine your output.
- `showOnly`: returns only the values whose status you are interested in. It takes two parameters: `statuses` and `granularity` - `showOnly`: returns only the values whose status you are interested in. It takes two parameters: `statuses` and `granularity`
`statuses` are the status you want to see in the output (e.g. `["added", "equal"]`) `statuses` are the status you want to see in the output (e.g. `["added", "equal"]`)
`granularity` can be either `basic` (to return only the main properties whose status matches your query) or `deep` (to return the main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly). `granularity` can be either `basic` (to return only the main properties whose status matches your query) or `deep` (to return the main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly).
@ -237,23 +219,20 @@ function getSubPropertiesDiff(
export function getObjectDiff( export function getObjectDiff(
prevData: ObjectData, prevData: ObjectData,
nextData: ObjectData, nextData: ObjectData,
options: ObjectOptions = { options: ObjectDiffOptions = DEFAULT_OBJECT_DIFF_OPTIONS,
ignoreArrayOrder: false,
showOnly: { statuses: [], granularity: GRANULARITY.BASIC },
},
): ObjectDiff { ): ObjectDiff {
if (!prevData && !nextData) { if (!prevData && !nextData) {
return { return {
type: "object", type: "object",
status: STATUS.EQUAL, status: OBJECT_STATUS.EQUAL,
diff: [], diff: [],
}; };
} }
if (!prevData) { if (!prevData) {
return formatSingleObjectDiff(nextData, STATUS.ADDED, options); return formatSingleObjectDiff(nextData, OBJECT_STATUS.ADDED, options);
} }
if (!nextData) { if (!nextData) {
return formatSingleObjectDiff(prevData, STATUS.DELETED, options); return formatSingleObjectDiff(prevData, OBJECT_STATUS.DELETED, options);
} }
const diff: ObjectDiff["diff"] = []; const diff: ObjectDiff["diff"] = [];
Object.entries(nextData).forEach(([nextProperty, nextValue]) => { Object.entries(nextData).forEach(([nextProperty, nextValue]) => {
@ -264,15 +243,15 @@ export function getObjectDiff(
previousValue, previousValue,
currentValue: nextValue, currentValue: nextValue,
status: !(nextProperty in prevData) status: !(nextProperty in prevData)
? STATUS.ADDED ? OBJECT_STATUS.ADDED
: previousValue === nextValue : previousValue === nextValue
? STATUS.EQUAL ? OBJECT_STATUS.EQUAL
: STATUS.UPDATED, : OBJECT_STATUS.UPDATED,
}); });
} }
if (isObject(nextValue)) { if (isObject(nextValue)) {
const subPropertiesDiff: SubProperties[] = getSubPropertiesDiff( const subPropertiesDiff: Diff[] = getSubPropertiesDiff(
previousValue, previousValue as Record<string, unknown>,
nextValue, nextValue,
options, options,
); );
@ -282,7 +261,9 @@ export function getObjectDiff(
previousValue, previousValue,
currentValue: nextValue, currentValue: nextValue,
status: subPropertyStatus, status: subPropertyStatus,
...(subPropertyStatus !== STATUS.EQUAL && { subPropertiesDiff }), ...(subPropertyStatus !== OBJECT_STATUS.EQUAL && {
diff: subPropertiesDiff,
}),
}); });
} }
return diff.push({ return diff.push({
@ -299,7 +280,7 @@ export function getObjectDiff(
property: deletedProperty.property, property: deletedProperty.property,
previousValue: deletedProperty.value, previousValue: deletedProperty.value,
currentValue: undefined, currentValue: undefined,
status: STATUS.DELETED, status: OBJECT_STATUS.DELETED,
}); });
}); });
} }

18
src/utils.ts

@ -1,15 +1,15 @@
import { isEqualOptions } from "./model"; import { isEqualOptions } from "./models/utils";
/** /**
* Returns true if two data are equal * Returns true if two data are equal
* @param {any} a - The original data. * @param {unknown} a - The original data.
* @param {any} b - The data to compare. * @param {unknown} b - The data to compare.
* @param {isEqualOptions} options - The options to compare the data. * @param {isEqualOptions} options - The options to compare the data.
* @returns boolean * @returns boolean
*/ */
export function isEqual( export function isEqual(
a: any, a: unknown,
b: any, b: unknown,
options: isEqualOptions = { ignoreArrayOrder: false }, options: isEqualOptions = { ignoreArrayOrder: false },
): boolean { ): boolean {
if (typeof a !== typeof b) return false; if (typeof a !== typeof b) return false;
@ -19,7 +19,7 @@ export function isEqual(
} }
if (options.ignoreArrayOrder) { if (options.ignoreArrayOrder) {
return a.every((v) => return a.every((v) =>
b.some((nextV: any) => JSON.stringify(nextV) === JSON.stringify(v)), b.some((nextV) => JSON.stringify(nextV) === JSON.stringify(v)),
); );
} }
return a.every((v, i) => JSON.stringify(v) === JSON.stringify(b[i])); return a.every((v, i) => JSON.stringify(v) === JSON.stringify(b[i]));
@ -32,9 +32,9 @@ export function isEqual(
/** /**
* Returns true if the provided value is an object * Returns true if the provided value is an object
* @param {any} value - The data to check. * @param {unknown} value - The data to check.
* @returns value is Record<string, any> * @returns value is Record<string, unknown>
*/ */
export function isObject(value: any): value is Record<string, any> { export function isObject(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value); return !!value && typeof value === "object" && !Array.isArray(value);
} }

7
test/list-diff.test.ts

@ -1,4 +1,5 @@
import { getListDiff } from "../src/list-diff"; import { getListDiff } from "../src/list-diff";
import { LIST_STATUS } from "../src/models/list";
describe("getListDiff", () => { describe("getListDiff", () => {
it("returns an empty diff if no lists are provided", () => { it("returns an empty diff if no lists are provided", () => {
@ -416,7 +417,7 @@ describe("getListDiff", () => {
false, false,
{ name: "joe", age: 88 }, { name: "joe", age: 88 },
], ],
{ showOnly: ["added", "deleted"] }, { showOnly: [LIST_STATUS.ADDED, LIST_STATUS.DELETED] },
), ),
).toStrictEqual({ ).toStrictEqual({
type: "list", type: "list",
@ -461,7 +462,7 @@ describe("getListDiff", () => {
}); });
expect( expect(
getListDiff(["mbappe", "mendes", "verratti", "ruiz"], null, { getListDiff(["mbappe", "mendes", "verratti", "ruiz"], null, {
showOnly: ["moved", "updated"], showOnly: [LIST_STATUS.MOVED, LIST_STATUS.UPDATED],
}), }),
).toStrictEqual({ ).toStrictEqual({
type: "list", type: "list",
@ -472,7 +473,7 @@ describe("getListDiff", () => {
it("returns all values if their status match the required statuses", () => { it("returns all values if their status match the required statuses", () => {
expect( expect(
getListDiff(null, ["mbappe", "mendes", "verratti", "ruiz"], { getListDiff(null, ["mbappe", "mendes", "verratti", "ruiz"], {
showOnly: ["added"], showOnly: [LIST_STATUS.ADDED],
}), }),
).toStrictEqual({ ).toStrictEqual({
type: "list", type: "list",

58
test/object-diff.test.ts

@ -1,3 +1,4 @@
import { GRANULARITY, OBJECT_STATUS } from "../src/models/object";
import { getObjectDiff } from "../src/object-diff"; import { getObjectDiff } from "../src/object-diff";
describe("getObjectDiff", () => { describe("getObjectDiff", () => {
@ -189,7 +190,7 @@ describe("getObjectDiff", () => {
nickname: "super joe", nickname: "super joe",
}, },
status: "updated", status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "age", property: "age",
previousValue: 66, previousValue: 66,
@ -294,7 +295,7 @@ describe("getObjectDiff", () => {
}, },
}, },
status: "updated", status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "name", property: "name",
previousValue: "joe", previousValue: "joe",
@ -318,7 +319,7 @@ describe("getObjectDiff", () => {
}, },
}, },
status: "updated", status: "updated",
subDiff: [ diff: [
{ {
property: "member", property: "member",
previousValue: true, previousValue: true,
@ -336,7 +337,7 @@ describe("getObjectDiff", () => {
golf: ["st andrews"], golf: ["st andrews"],
}, },
status: "updated", status: "updated",
subDiff: [ diff: [
{ {
property: "rugby", property: "rugby",
previousValue: ["france"], previousValue: ["france"],
@ -420,7 +421,7 @@ describe("getObjectDiff", () => {
nickname: "super joe", nickname: "super joe",
}, },
status: "updated", status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "age", property: "age",
previousValue: 66, previousValue: 66,
@ -485,7 +486,7 @@ describe("getObjectDiff", () => {
nickname: "super joe", nickname: "super joe",
}, },
}, },
{ showOnly: { statuses: ["added"] } }, { showOnly: { statuses: [OBJECT_STATUS.ADDED] } },
), ),
).toStrictEqual({ ).toStrictEqual({
type: "object", type: "object",
@ -523,7 +524,12 @@ describe("getObjectDiff", () => {
nickname: "super joe", nickname: "super joe",
}, },
}, },
{ showOnly: { statuses: ["added", "deleted"], granularity: "deep" } }, {
showOnly: {
statuses: [OBJECT_STATUS.ADDED, OBJECT_STATUS.DELETED],
granularity: GRANULARITY.DEEP,
},
},
), ),
).toStrictEqual({ ).toStrictEqual({
type: "object", type: "object",
@ -550,7 +556,7 @@ describe("getObjectDiff", () => {
nickname: "super joe", nickname: "super joe",
}, },
status: "updated", status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "age", property: "age",
previousValue: 66, previousValue: 66,
@ -605,8 +611,8 @@ describe("getObjectDiff", () => {
}, },
{ {
showOnly: { showOnly: {
statuses: ["updated"], statuses: [OBJECT_STATUS.UPDATED],
granularity: "deep", granularity: GRANULARITY.DEEP,
}, },
}, },
), ),
@ -637,7 +643,7 @@ describe("getObjectDiff", () => {
}, },
}, },
status: "updated", status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "data", property: "data",
previousValue: { previousValue: {
@ -655,7 +661,7 @@ describe("getObjectDiff", () => {
}, },
}, },
status: "updated", status: "updated",
subDiff: [ diff: [
{ {
property: "hobbies", property: "hobbies",
previousValue: { previousValue: {
@ -667,7 +673,7 @@ describe("getObjectDiff", () => {
golf: ["st andrews"], golf: ["st andrews"],
}, },
status: "updated", status: "updated",
subDiff: [ diff: [
{ {
property: "football", property: "football",
previousValue: ["psg"], previousValue: ["psg"],
@ -713,8 +719,8 @@ describe("getObjectDiff", () => {
}, },
{ {
showOnly: { showOnly: {
statuses: ["added"], statuses: [OBJECT_STATUS.ADDED],
granularity: "deep", granularity: GRANULARITY.DEEP,
}, },
}, },
), ),
@ -744,7 +750,7 @@ describe("getObjectDiff", () => {
}, },
}, },
status: "updated", status: "updated",
subPropertiesDiff: [ diff: [
{ {
property: "data", property: "data",
previousValue: { previousValue: {
@ -761,7 +767,7 @@ describe("getObjectDiff", () => {
}, },
}, },
status: "updated", status: "updated",
subDiff: [ diff: [
{ {
property: "hobbies", property: "hobbies",
previousValue: { previousValue: {
@ -772,7 +778,7 @@ describe("getObjectDiff", () => {
golf: ["st andrews"], golf: ["st andrews"],
}, },
status: "updated", status: "updated",
subDiff: [ diff: [
{ {
property: "football", property: "football",
previousValue: undefined, previousValue: undefined,
@ -803,7 +809,12 @@ describe("getObjectDiff", () => {
age: 54, age: 54,
hobbies: ["golf", "football"], hobbies: ["golf", "football"],
}, },
{ showOnly: { statuses: ["deleted"], granularity: "deep" } }, {
showOnly: {
statuses: [OBJECT_STATUS.DELETED],
granularity: GRANULARITY.DEEP,
},
},
), ),
).toStrictEqual({ ).toStrictEqual({
type: "object", type: "object",
@ -819,7 +830,12 @@ describe("getObjectDiff", () => {
hobbies: ["golf", "football"], hobbies: ["golf", "football"],
}, },
null, null,
{ showOnly: { statuses: ["added"], granularity: "deep" } }, {
showOnly: {
statuses: [OBJECT_STATUS.ADDED],
granularity: GRANULARITY.DEEP,
},
},
), ),
).toStrictEqual({ ).toStrictEqual({
type: "object", type: "object",
@ -831,7 +847,7 @@ describe("getObjectDiff", () => {
getObjectDiff( getObjectDiff(
{ name: "joe", age: 54, hobbies: ["golf", "football"] }, { name: "joe", age: 54, hobbies: ["golf", "football"] },
null, null,
{ showOnly: { statuses: ["deleted"] } }, { showOnly: { statuses: [OBJECT_STATUS.DELETED] } },
), ),
).toStrictEqual({ ).toStrictEqual({
type: "object", type: "object",

Loading…
Cancel
Save