Browse Source

Merge pull request #10 from DoneDeal0/granular-output

feat: add granular output
pull/16/head
DoneDeal0 2 years ago committed by GitHub
parent
commit
9f32cade95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 126
      README.md
  2. 55
      dist/index.d.ts
  3. 10
      dist/index.js
  4. 4
      dist/index.mjs
  5. 10
      package.json
  6. 80
      src/list-diff.ts
  7. 70
      src/model.ts
  8. 112
      src/object-diff.ts
  9. 6
      src/utils.ts
  10. 116
      test/list-diff.test.ts
  11. 434
      test/object-diff.test.ts
  12. 20
      test/utils.test.ts
  13. 1056
      yarn.lock

126
README.md

@ -6,19 +6,10 @@ This library compares two arrays or objects and return a complete diff of their @@ -6,19 +6,10 @@ This library compares two arrays or objects and return a complete diff of their
## WHY YOU SHOULD USE THIS LIB
All other existing solutions return a weird diff format which often require an additional parsing. They are also slow and limited to object comparison. 👎
All other existing solutions return a weird diff format which often require an additional parsing. They are also limited to object comparison. 👎
**Superdiff** gives you a complete diff for both array <u>and</u> objects with a very readable format. Last but not least, it's battled tested and super fast. Import. Enjoy. 👍
**Benchmark**:
| Objects | Deep-diff 🐢 | Superdiff ⚡ |
| --------- | ------------ | ------------ |
| 1.000 | 10.47ms | 5.73ms |
| 10.000 | 43.05ms | 18.60ms |
| 100.000 | 289.71ms | 50.96ms |
| 1.000.000 | 2786.70ms | 389.78ms |
## DIFF FORMAT COMPARISON
Let's compare the diff format of **Superdiff** and **Deep-diff**, the most popular diff lib on npm:
@ -51,20 +42,19 @@ const objectB = { @@ -51,20 +42,19 @@ const objectB = {
```js
[
DiffEdit {
kind: 'E',
path: [ 'user', 'member' ],
lhs: true,
rhs: false
},
DiffEdit {
kind: 'E',
path: [ 'user', 'hobbies', 1 ],
lhs: 'football',
rhs: 'chess'
}
]
{
kind: "E",
path: ["user", "member"],
lhs: true,
rhs: false,
},
{
kind: "E",
path: ["user", "hobbies", 1],
lhs: "football",
rhs: "chess",
},
];
```
**SuperDiff** output:
@ -97,25 +87,25 @@ const objectB = { @@ -97,25 +87,25 @@ const objectB = {
+ status: "updated",
subPropertiesDiff: [
{
name: "name",
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
+ {
+ name: "member",
+ property: "member",
+ previousValue: true,
+ currentValue: false,
+ status: "updated",
+ },
+ {
+ name: "hobbies",
+ property: "hobbies",
+ previousValue: ["golf", "football"],
+ currentValue: ["golf", "chess"],
+ status: "updated",
+ },
{
name: "age",
property: "age",
previousValue: 66,
currentValue: 66,
status: "equal",
@ -148,25 +138,47 @@ format: @@ -148,25 +138,47 @@ format:
```ts
type ObjectDiff = {
type: "object";
status: "added" | "deleted" | "equal" | "moved" | "updated";
status: "added" | "deleted" | "equal" | "updated";
diff: {
property: string;
previousValue: any;
currentValue: any;
status: "added" | "deleted" | "equal" | "moved" | "updated";
status: "added" | "deleted" | "equal" | "updated";
// only appears if some subproperties have been added/deleted/updated
subPropertiesDiff?: {
name: string;
property: string;
previousValue: any;
currentValue: any;
status: "added" | "deleted" | "equal" | "moved" | "updated";
status: "added" | "deleted" | "equal" | "updated";
// subDiff is a recursive diff in case of nested subproperties
subDiff?: Subproperties[];
subDiff?: SubProperties[];
}[];
}[];
};
```
**Options**
You can add a third `options` parameter to `getObjectDiff`.
```ts
{
ignoreArrayOrder?: boolean // false by default,
showOnly?: {
statuses: ("added" | "deleted" | "updated" | "equal")[], // [] by default
granularity?: "basic" | "deep" // "basic" by default
}
}
```
- `ignoreArrayOrder`: if set to `true`, `["hello", "world"]` and `["world", "hello"]` will be considered as `equal`, because the two arrays have the same value, just not in the same order.
- `showOnly`: only returns the values whose status interest you. It has two parameters:
- `statuses`: status you want to see in the output (ex: `["added", "equal"]`)
- `granularity`:
- `basic` only returns the main properties whose status match your request.
- `deep` can return main properties if some of their subproperties' status match your request. The subproperties will be filtered accordingly.
### getListDiff()
```js
@ -197,6 +209,18 @@ type ListDiff = { @@ -197,6 +209,18 @@ type ListDiff = {
};
```
**Options**
You can add a third `options` parameter to `getListDiff`.
```ts
{
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
}
```
- `showOnly` gives you the option to only return the values whose status interest you (ex: `["added", "equal"]`).
### isEqual()
```js
@ -205,6 +229,18 @@ import { isEqual } from "@donedeal0/superdiff"; @@ -205,6 +229,18 @@ import { isEqual } from "@donedeal0/superdiff";
Checks if two values are equal.
**Options**
You can add a third `options` parameter to `isEqual`.
```ts
{
ignoreArrayOrder?: boolean // false by default,
}
```
- `ignoreArrayOrder`: if set to `true`, `["hello", "world"]` and `["world", "hello"]` will be considered as `equal`, because the two arrays have the same value, just not in the same order.
### isObject()
```js
@ -329,25 +365,25 @@ output @@ -329,25 +365,25 @@ output
+ status: "updated",
subPropertiesDiff: [
{
name: "name",
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
+ {
+ name: "member",
+ property: "member",
+ previousValue: true,
+ currentValue: false,
+ status: "updated",
+ },
+ {
+ name: "hobbies",
+ property: "hobbies",
+ previousValue: ["golf", "football"],
+ currentValue: ["golf", "chess"],
+ status: "updated",
+ },
{
name: "age",
property: "age",
previousValue: 66,
currentValue: 66,
status: "equal",
@ -393,29 +429,17 @@ output @@ -393,29 +429,17 @@ output
false;
```
More examples are availble in the tests of the source code.
More examples are available in the tests of the source code.
<hr/>
### OPTIONS
`getObjectDiff()` and `isEqual()` accept a facultative `options` parameter:
```ts
{
discardArrayOrder?: boolean // false by default
}
```
If `discardArrayOrder` is set to `true`, `["hello", "world"]` and `["world", "hello"]` will be considered as `equal`, because the two arrays have the same value, just not in the same order.
## CREDITS
DoneDeal0
## SUPPORT
If you use Superdiff, please show your support by buying me coffee:
If you or your company use Superdiff, please show your support by buying me a coffee:
https://www.buymeacoffee.com/donedeal0
<br/>

55
dist/index.d.ts vendored

@ -1,44 +1,69 @@ @@ -1,44 +1,69 @@
type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated";
declare const GRANULARITY: Record<string, "basic" | "deep">;
type ListDiffStatus = "added" | "equal" | "moved" | "deleted" | "updated";
type ObjectDiffStatus = "added" | "equal" | "deleted" | "updated";
type ObjectData = Record<string, any> | undefined | null;
type ListData = any;
type Options = {
discardArrayOrder?: boolean;
type ObjectStatusTuple = readonly [
"added",
"equal",
"deleted",
"updated"
];
type ListStatusTuple = readonly [
"added",
"equal",
"deleted",
"moved",
"updated"
];
type isEqualOptions = {
ignoreArrayOrder?: boolean;
};
type ObjectOptions = {
ignoreArrayOrder?: boolean;
showOnly?: {
statuses: Array<ObjectStatusTuple[number]>;
granularity?: typeof GRANULARITY[keyof typeof GRANULARITY];
};
};
type ListOptions = {
showOnly?: Array<ListStatusTuple[number]>;
};
type ListDiff = {
type: "list";
status: DiffStatus;
status: ListDiffStatus;
diff: {
value: ListData;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: DiffStatus;
status: ListDiffStatus;
}[];
};
type Subproperties = {
name: string;
type SubProperties = {
property: string;
previousValue: any;
currentValue: any;
status: DiffStatus;
subDiff?: Subproperties[];
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
};
type ObjectDiff = {
type: "object";
status: DiffStatus;
status: ObjectDiffStatus;
diff: {
property: string;
previousValue: any;
currentValue: any;
status: DiffStatus;
subPropertiesDiff?: Subproperties[];
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
}[];
};
declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: Options): ObjectDiff;
declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: ObjectOptions): ObjectDiff;
declare const getListDiff: (prevList: ListData[] | undefined | null, nextList: ListData[] | undefined | null) => ListDiff;
declare const getListDiff: (prevList: ListData[] | undefined | null, nextList: ListData[] | undefined | null, options?: ListOptions) => ListDiff;
declare function isEqual(a: any, b: any, options?: Options): boolean;
declare function isEqual(a: any, b: any, options?: isEqualOptions): boolean;
declare function isObject(value: any): value is Record<string, any>;
export { getListDiff, getObjectDiff, isEqual, isObject };

10
dist/index.js vendored

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
'use strict';
var r={ADDED:"added",EQUAL:"equal",MOVED:"moved",DELETED:"deleted",UPDATED:"updated"};function d(e,t,n={discardArrayOrder:!1}){return typeof e!=typeof t?!1:Array.isArray(e)?e.length!==t.length?!1:n.discardArrayOrder?e.every(i=>t.some(s=>JSON.stringify(s)===JSON.stringify(i))):e.every((i,s)=>JSON.stringify(i)===JSON.stringify(t[s])):typeof e=="object"?JSON.stringify(e)===JSON.stringify(t):e===t}function p(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function A(e){return e.some(t=>t.status!==r.EQUAL)?r.UPDATED:r.EQUAL}function E(e,t){if(!e)return {type:"object",status:r.isEqual,diff:[]};let n=[];return Object.entries(e).forEach(([i,s])=>{if(p(s)){let f=[];return Object.entries(s).forEach(([u,o])=>{f.push({name:u,previousValue:t===r.ADDED?void 0:o,currentValue:t===r.ADDED?o:void 0,status:t});}),n.push({property:i,previousValue:t===r.ADDED?void 0:e[i],currentValue:t===r.ADDED?s:void 0,status:t,subPropertiesDiff:f})}return n.push({property:i,previousValue:t===r.ADDED?void 0:e[i],currentValue:t===r.ADDED?s:void 0,status:t})}),{type:"object",status:t,diff:n}}function S(e,t,n){if(!e)return;let i=Object.entries(e).find(([s])=>d(s,t,n));return i?i[1]:void 0}function l(e,t,n){return d(e,t,n)?r.EQUAL:r.UPDATED}function j(e){return e.some(t=>t.status!==r.EQUAL)?r.UPDATED:r.EQUAL}function y(e,t){if(!e)return;let n=Object.keys(e),i=Object.keys(t),s=n.filter(f=>!i.includes(f));if(s.length>0)return s.map(f=>({property:f,value:e[f]}))}function b(e,t,n){let i=[],s,f=y(e,t);return f&&f.forEach(u=>{i.push({name:u.property,previousValue:u.value,currentValue:void 0,status:r.DELETED});}),Object.entries(t).forEach(([u,o])=>{let D=S(e,u,n);if(!D)return i.push({name:u,previousValue:D,currentValue:o,status:!e||!(u in e)?r.ADDED:D===o?r.EQUAL:r.UPDATED});if(p(o)){let a=b(D,o,n);a&&a.length>0&&(s=a);}D&&i.push({name:u,previousValue:D,currentValue:o,status:l(D,o,n),...!!s&&{subDiff:s}});}),i}function g(e,t,n){if(!e&&!t)return {type:"object",status:r.EQUAL,diff:[]};if(!e)return E(t,r.ADDED);if(!t)return E(e,r.DELETED);let i=[];Object.entries(t).forEach(([f,u])=>{let o=e[f];if(!o)return i.push({property:f,previousValue:o,currentValue:u,status:f in e?o===u?r.EQUAL:r.UPDATED:r.ADDED});if(p(u)){let D=b(o,u,n),a=j(D);return i.push({property:f,previousValue:o,currentValue:u,status:a,...a!==r.EQUAL&&{subPropertiesDiff:D}})}return i.push({property:f,previousValue:o,currentValue:u,status:l(o,u,n)})});let s=y(e,t);return s&&s.forEach(f=>{i.push({property:f.property,previousValue:f.value,currentValue:void 0,status:r.DELETED});}),{type:"object",status:A(i),diff:i}}function O(e,t){return {type:"list",status:t,diff:e.map((n,i)=>({value:n,prevIndex:t===r.ADDED?null:i,newIndex:t===r.ADDED?i:null,indexDiff:null,status:t}))}}function L(e){return e.some(t=>t.status!==r.EQUAL)?r.UPDATED:r.EQUAL}var m=(e,t)=>{if(!e&&!t)return {type:"list",status:r.EQUAL,diff:[]};if(!e)return O(t,r.ADDED);if(!t)return O(e,r.DELETED);let n=[],i=[];return t.forEach((s,f)=>{let u=e.findIndex((D,a)=>d(D,s)&&!i.includes(a));u>-1&&i.push(u);let o=u===-1?null:f-u;return o===0?n.push({value:s,prevIndex:u,newIndex:f,indexDiff:o,status:r.EQUAL}):u===-1?n.push({value:s,prevIndex:null,newIndex:f,indexDiff:o,status:r.ADDED}):n.push({value:s,prevIndex:u,newIndex:f,indexDiff:o,status:r.MOVED})}),e.forEach((s,f)=>{if(!i.includes(f))return n.splice(f,0,{value:s,prevIndex:f,newIndex:null,indexDiff:null,status:r.DELETED})}),{type:"list",status:L(n),diff:n}};
var u={ADDED:"added",EQUAL:"equal",DELETED:"deleted",UPDATED:"updated"},d={...u,MOVED:"moved"},c={BASIC:"basic",DEEP:"deep"};function p(t,e,f={ignoreArrayOrder:!1}){return typeof t!=typeof e?!1:Array.isArray(t)?t.length!==e.length?!1:f.ignoreArrayOrder?t.every(r=>e.some(s=>JSON.stringify(s)===JSON.stringify(r))):t.every((r,s)=>JSON.stringify(r)===JSON.stringify(e[s])):typeof t=="object"?JSON.stringify(t)===JSON.stringify(e):t===e}function l(t){return !!t&&typeof t=="object"&&!Array.isArray(t)}function O(t,e={statuses:[],granularity:c.BASIC}){let{statuses:f,granularity:r}=e;return t.reduce((s,i)=>{if(r===c.DEEP&&i.subPropertiesDiff){let n=O(i.subPropertiesDiff,e);if(n.length>0)return [...s,{...i,subPropertiesDiff:n}]}if(r===c.DEEP&&i.subDiff){let n=O(i.subDiff,e);if(n.length>0)return [...s,{...i,subDiff:n}]}return f.includes(i.status)?[...s,i]:s},[])}function b(t){return t.some(e=>e.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function E(t,e,f={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:c.BASIC}}){if(!t)return {type:"object",status:u.EQUAL,diff:[]};let r=[];return Object.entries(t).forEach(([s,i])=>{if(l(i)){let n=[];return Object.entries(i).forEach(([o,a])=>{n.push({property:o,previousValue:e===u.ADDED?void 0:a,currentValue:e===u.ADDED?a:void 0,status:e});}),r.push({property:s,previousValue:e===u.ADDED?void 0:t[s],currentValue:e===u.ADDED?i:void 0,status:e,subPropertiesDiff:n})}return r.push({property:s,previousValue:e===u.ADDED?void 0:t[s],currentValue:e===u.ADDED?i:void 0,status:e})}),f.showOnly&&f.showOnly.statuses.length>0?{type:"object",status:e,diff:O(r,f.showOnly)}:{type:"object",status:e,diff:r}}function T(t,e,f){if(!t)return;let r=Object.entries(t).find(([s])=>p(s,e,f));return r?r[1]:void 0}function A(t,e,f){return p(t,e,f)?u.EQUAL:u.UPDATED}function m(t){return t.some(e=>e.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function j(t,e){if(!t)return;let f=Object.keys(t),r=Object.keys(e),s=f.filter(i=>!r.includes(i));if(s.length>0)return s.map(i=>({property:i,value:t[i]}))}function L(t,e,f){let r=[],s,i=j(t,e);return i&&i.forEach(n=>{r.push({property:n.property,previousValue:n.value,currentValue:void 0,status:u.DELETED});}),Object.entries(e).forEach(([n,o])=>{let a=T(t,n,f);if(!a)return r.push({property:n,previousValue:a,currentValue:o,status:!t||!(n in t)?u.ADDED:a===o?u.EQUAL:u.UPDATED});if(l(o)){let D=L(a,o,f);D&&D.length>0&&(s=D);}a&&r.push({property:n,previousValue:a,currentValue:o,status:A(a,o,f),...!!s&&{subDiff:s}});}),r}function U(t,e,f={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:c.BASIC}}){if(!t&&!e)return {type:"object",status:u.EQUAL,diff:[]};if(!t)return E(e,u.ADDED,f);if(!e)return E(t,u.DELETED,f);let r=[];Object.entries(e).forEach(([i,n])=>{let o=t[i];if(!o)return r.push({property:i,previousValue:o,currentValue:n,status:i in t?o===n?u.EQUAL:u.UPDATED:u.ADDED});if(l(n)){let a=L(o,n,f),D=m(a);return r.push({property:i,previousValue:o,currentValue:n,status:D,...D!==u.EQUAL&&{subPropertiesDiff:a}})}return r.push({property:i,previousValue:o,currentValue:n,status:A(o,n,f)})});let s=j(t,e);return s&&s.forEach(i=>{r.push({property:i.property,previousValue:i.value,currentValue:void 0,status:u.DELETED});}),f.showOnly&&f.showOnly.statuses.length>0?{type:"object",status:b(r),diff:O(r,f.showOnly)}:{type:"object",status:b(r),diff:r}}function P(t,e=[]){return t.filter(f=>e?.includes(f.status))}function S(t,e,f={showOnly:[]}){let r=t.map((s,i)=>({value:s,prevIndex:e===d.ADDED?null:i,newIndex:e===d.ADDED?i:null,indexDiff:null,status:e}));return f.showOnly&&f.showOnly.length>0?{type:"list",status:e,diff:r.filter(s=>f.showOnly?.includes(s.status))}:{type:"list",status:e,diff:r}}function g(t){return t.some(e=>e.status!==d.EQUAL)?d.UPDATED:d.EQUAL}var w=(t,e,f={showOnly:[]})=>{if(!t&&!e)return {type:"list",status:d.EQUAL,diff:[]};if(!t)return S(e,d.ADDED,f);if(!e)return S(t,d.DELETED,f);let r=[],s=[];return e.forEach((i,n)=>{let o=t.findIndex((D,h)=>p(D,i)&&!s.includes(h));o>-1&&s.push(o);let a=o===-1?null:n-o;return a===0?r.push({value:i,prevIndex:o,newIndex:n,indexDiff:a,status:d.EQUAL}):o===-1?r.push({value:i,prevIndex:null,newIndex:n,indexDiff:a,status:d.ADDED}):r.push({value:i,prevIndex:o,newIndex:n,indexDiff:a,status:d.MOVED})}),t.forEach((i,n)=>{if(!s.includes(n))return r.splice(n,0,{value:i,prevIndex:n,newIndex:null,indexDiff:null,status:d.DELETED})}),f.showOnly&&f?.showOnly?.length>0?{type:"list",status:g(r),diff:P(r,f.showOnly)}:{type:"list",status:g(r),diff:r}};
exports.getListDiff = m;
exports.getObjectDiff = g;
exports.isEqual = d;
exports.isObject = p;
exports.getListDiff = w;
exports.getObjectDiff = U;
exports.isEqual = p;
exports.isObject = l;

4
dist/index.mjs vendored

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
var r={ADDED:"added",EQUAL:"equal",MOVED:"moved",DELETED:"deleted",UPDATED:"updated"};function d(e,t,n={discardArrayOrder:!1}){return typeof e!=typeof t?!1:Array.isArray(e)?e.length!==t.length?!1:n.discardArrayOrder?e.every(i=>t.some(s=>JSON.stringify(s)===JSON.stringify(i))):e.every((i,s)=>JSON.stringify(i)===JSON.stringify(t[s])):typeof e=="object"?JSON.stringify(e)===JSON.stringify(t):e===t}function p(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function S(e){return e.some(t=>t.status!==r.EQUAL)?r.UPDATED:r.EQUAL}function l(e,t){if(!e)return {type:"object",status:r.isEqual,diff:[]};let n=[];return Object.entries(e).forEach(([i,s])=>{if(p(s)){let f=[];return Object.entries(s).forEach(([u,o])=>{f.push({name:u,previousValue:t===r.ADDED?void 0:o,currentValue:t===r.ADDED?o:void 0,status:t});}),n.push({property:i,previousValue:t===r.ADDED?void 0:e[i],currentValue:t===r.ADDED?s:void 0,status:t,subPropertiesDiff:f})}return n.push({property:i,previousValue:t===r.ADDED?void 0:e[i],currentValue:t===r.ADDED?s:void 0,status:t})}),{type:"object",status:t,diff:n}}function j(e,t,n){if(!e)return;let i=Object.entries(e).find(([s])=>d(s,t,n));return i?i[1]:void 0}function y(e,t,n){return d(e,t,n)?r.EQUAL:r.UPDATED}function g(e){return e.some(t=>t.status!==r.EQUAL)?r.UPDATED:r.EQUAL}function b(e,t){if(!e)return;let n=Object.keys(e),i=Object.keys(t),s=n.filter(f=>!i.includes(f));if(s.length>0)return s.map(f=>({property:f,value:e[f]}))}function O(e,t,n){let i=[],s,f=b(e,t);return f&&f.forEach(u=>{i.push({name:u.property,previousValue:u.value,currentValue:void 0,status:r.DELETED});}),Object.entries(t).forEach(([u,o])=>{let D=j(e,u,n);if(!D)return i.push({name:u,previousValue:D,currentValue:o,status:!e||!(u in e)?r.ADDED:D===o?r.EQUAL:r.UPDATED});if(p(o)){let a=O(D,o,n);a&&a.length>0&&(s=a);}D&&i.push({name:u,previousValue:D,currentValue:o,status:y(D,o,n),...!!s&&{subDiff:s}});}),i}function L(e,t,n){if(!e&&!t)return {type:"object",status:r.EQUAL,diff:[]};if(!e)return l(t,r.ADDED);if(!t)return l(e,r.DELETED);let i=[];Object.entries(t).forEach(([f,u])=>{let o=e[f];if(!o)return i.push({property:f,previousValue:o,currentValue:u,status:f in e?o===u?r.EQUAL:r.UPDATED:r.ADDED});if(p(u)){let D=O(o,u,n),a=g(D);return i.push({property:f,previousValue:o,currentValue:u,status:a,...a!==r.EQUAL&&{subPropertiesDiff:D}})}return i.push({property:f,previousValue:o,currentValue:u,status:y(o,u,n)})});let s=b(e,t);return s&&s.forEach(f=>{i.push({property:f.property,previousValue:f.value,currentValue:void 0,status:r.DELETED});}),{type:"object",status:S(i),diff:i}}function A(e,t){return {type:"list",status:t,diff:e.map((n,i)=>({value:n,prevIndex:t===r.ADDED?null:i,newIndex:t===r.ADDED?i:null,indexDiff:null,status:t}))}}function m(e){return e.some(t=>t.status!==r.EQUAL)?r.UPDATED:r.EQUAL}var h=(e,t)=>{if(!e&&!t)return {type:"list",status:r.EQUAL,diff:[]};if(!e)return A(t,r.ADDED);if(!t)return A(e,r.DELETED);let n=[],i=[];return t.forEach((s,f)=>{let u=e.findIndex((D,a)=>d(D,s)&&!i.includes(a));u>-1&&i.push(u);let o=u===-1?null:f-u;return o===0?n.push({value:s,prevIndex:u,newIndex:f,indexDiff:o,status:r.EQUAL}):u===-1?n.push({value:s,prevIndex:null,newIndex:f,indexDiff:o,status:r.ADDED}):n.push({value:s,prevIndex:u,newIndex:f,indexDiff:o,status:r.MOVED})}),e.forEach((s,f)=>{if(!i.includes(f))return n.splice(f,0,{value:s,prevIndex:f,newIndex:null,indexDiff:null,status:r.DELETED})}),{type:"list",status:m(n),diff:n}};
var u={ADDED:"added",EQUAL:"equal",DELETED:"deleted",UPDATED:"updated"},d={...u,MOVED:"moved"},c={BASIC:"basic",DEEP:"deep"};function p(t,e,f={ignoreArrayOrder:!1}){return typeof t!=typeof e?!1:Array.isArray(t)?t.length!==e.length?!1:f.ignoreArrayOrder?t.every(r=>e.some(s=>JSON.stringify(s)===JSON.stringify(r))):t.every((r,s)=>JSON.stringify(r)===JSON.stringify(e[s])):typeof t=="object"?JSON.stringify(t)===JSON.stringify(e):t===e}function l(t){return !!t&&typeof t=="object"&&!Array.isArray(t)}function b(t,e={statuses:[],granularity:c.BASIC}){let{statuses:f,granularity:r}=e;return t.reduce((s,i)=>{if(r===c.DEEP&&i.subPropertiesDiff){let n=b(i.subPropertiesDiff,e);if(n.length>0)return [...s,{...i,subPropertiesDiff:n}]}if(r===c.DEEP&&i.subDiff){let n=b(i.subDiff,e);if(n.length>0)return [...s,{...i,subDiff:n}]}return f.includes(i.status)?[...s,i]:s},[])}function E(t){return t.some(e=>e.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function A(t,e,f={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:c.BASIC}}){if(!t)return {type:"object",status:u.EQUAL,diff:[]};let r=[];return Object.entries(t).forEach(([s,i])=>{if(l(i)){let n=[];return Object.entries(i).forEach(([o,a])=>{n.push({property:o,previousValue:e===u.ADDED?void 0:a,currentValue:e===u.ADDED?a:void 0,status:e});}),r.push({property:s,previousValue:e===u.ADDED?void 0:t[s],currentValue:e===u.ADDED?i:void 0,status:e,subPropertiesDiff:n})}return r.push({property:s,previousValue:e===u.ADDED?void 0:t[s],currentValue:e===u.ADDED?i:void 0,status:e})}),f.showOnly&&f.showOnly.statuses.length>0?{type:"object",status:e,diff:b(r,f.showOnly)}:{type:"object",status:e,diff:r}}function m(t,e,f){if(!t)return;let r=Object.entries(t).find(([s])=>p(s,e,f));return r?r[1]:void 0}function j(t,e,f){return p(t,e,f)?u.EQUAL:u.UPDATED}function U(t){return t.some(e=>e.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function L(t,e){if(!t)return;let f=Object.keys(t),r=Object.keys(e),s=f.filter(i=>!r.includes(i));if(s.length>0)return s.map(i=>({property:i,value:t[i]}))}function S(t,e,f){let r=[],s,i=L(t,e);return i&&i.forEach(n=>{r.push({property:n.property,previousValue:n.value,currentValue:void 0,status:u.DELETED});}),Object.entries(e).forEach(([n,o])=>{let a=m(t,n,f);if(!a)return r.push({property:n,previousValue:a,currentValue:o,status:!t||!(n in t)?u.ADDED:a===o?u.EQUAL:u.UPDATED});if(l(o)){let D=S(a,o,f);D&&D.length>0&&(s=D);}a&&r.push({property:n,previousValue:a,currentValue:o,status:j(a,o,f),...!!s&&{subDiff:s}});}),r}function P(t,e,f={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:c.BASIC}}){if(!t&&!e)return {type:"object",status:u.EQUAL,diff:[]};if(!t)return A(e,u.ADDED,f);if(!e)return A(t,u.DELETED,f);let r=[];Object.entries(e).forEach(([i,n])=>{let o=t[i];if(!o)return r.push({property:i,previousValue:o,currentValue:n,status:i in t?o===n?u.EQUAL:u.UPDATED:u.ADDED});if(l(n)){let a=S(o,n,f),D=U(a);return r.push({property:i,previousValue:o,currentValue:n,status:D,...D!==u.EQUAL&&{subPropertiesDiff:a}})}return r.push({property:i,previousValue:o,currentValue:n,status:j(o,n,f)})});let s=L(t,e);return s&&s.forEach(i=>{r.push({property:i.property,previousValue:i.value,currentValue:void 0,status:u.DELETED});}),f.showOnly&&f.showOnly.statuses.length>0?{type:"object",status:E(r),diff:b(r,f.showOnly)}:{type:"object",status:E(r),diff:r}}function w(t,e=[]){return t.filter(f=>e?.includes(f.status))}function g(t,e,f={showOnly:[]}){let r=t.map((s,i)=>({value:s,prevIndex:e===d.ADDED?null:i,newIndex:e===d.ADDED?i:null,indexDiff:null,status:e}));return f.showOnly&&f.showOnly.length>0?{type:"list",status:e,diff:r.filter(s=>f.showOnly?.includes(s.status))}:{type:"list",status:e,diff:r}}function h(t){return t.some(e=>e.status!==d.EQUAL)?d.UPDATED:d.EQUAL}var I=(t,e,f={showOnly:[]})=>{if(!t&&!e)return {type:"list",status:d.EQUAL,diff:[]};if(!t)return g(e,d.ADDED,f);if(!e)return g(t,d.DELETED,f);let r=[],s=[];return e.forEach((i,n)=>{let o=t.findIndex((D,T)=>p(D,i)&&!s.includes(T));o>-1&&s.push(o);let a=o===-1?null:n-o;return a===0?r.push({value:i,prevIndex:o,newIndex:n,indexDiff:a,status:d.EQUAL}):o===-1?r.push({value:i,prevIndex:null,newIndex:n,indexDiff:a,status:d.ADDED}):r.push({value:i,prevIndex:o,newIndex:n,indexDiff:a,status:d.MOVED})}),t.forEach((i,n)=>{if(!s.includes(n))return r.splice(n,0,{value:i,prevIndex:n,newIndex:null,indexDiff:null,status:d.DELETED})}),f.showOnly&&f?.showOnly?.length>0?{type:"list",status:h(r),diff:w(r,f.showOnly)}:{type:"list",status:h(r),diff:r}};
export { h as getListDiff, L as getObjectDiff, d as isEqual, p as isObject };
export { I as getListDiff, P as getObjectDiff, p as isEqual, l as isObject };

10
package.json

@ -37,10 +37,10 @@ @@ -37,10 +37,10 @@
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@types/jest": "^29.2.4",
"jest": "^29.3.1",
"ts-jest": "^29.0.3",
"tsup": "^6.5.0",
"typescript": "^4.9.4"
"@types/jest": "^29.4.0",
"jest": "^29.4.2",
"ts-jest": "^29.0.5",
"tsup": "^6.6.0",
"typescript": "^4.9.5"
}
}

80
src/list-diff.ts

@ -1,45 +1,76 @@ @@ -1,45 +1,76 @@
import { STATUS, ListDiff, ListData, DiffStatus } from "./model";
import {
LIST_STATUS,
ListData,
ListDiff,
ListDiffStatus,
ListOptions,
} from "./model";
import { isEqual } from "./utils";
function getLeanDiff(
diff: ListDiff["diff"],
showOnly = [] as ListOptions["showOnly"]
): ListDiff["diff"] {
return diff.filter((value) => showOnly?.includes(value.status));
}
function formatSingleListDiff(
listData: ListData[],
status: DiffStatus
status: ListDiffStatus,
options: ListOptions = { showOnly: [] }
): ListDiff {
const diff = listData.map((data: ListData, i) => ({
value: data,
prevIndex: status === LIST_STATUS.ADDED ? null : i,
newIndex: status === LIST_STATUS.ADDED ? i : null,
indexDiff: null,
status,
}));
if (options.showOnly && options.showOnly.length > 0) {
return {
type: "list",
status,
diff: diff.filter((value) => options.showOnly?.includes(value.status)),
};
}
return {
type: "list",
status,
diff: listData.map((data: ListData, i) => ({
value: data,
prevIndex: status === STATUS.ADDED ? null : i,
newIndex: status === STATUS.ADDED ? i : null,
indexDiff: null,
status,
})),
diff,
};
}
function getListStatus(listDiff: ListDiff["diff"]): DiffStatus {
return listDiff.some((value) => value.status !== STATUS.EQUAL)
? STATUS.UPDATED
: STATUS.EQUAL;
function getListStatus(listDiff: ListDiff["diff"]): ListDiffStatus {
return listDiff.some((value) => value.status !== LIST_STATUS.EQUAL)
? LIST_STATUS.UPDATED
: LIST_STATUS.EQUAL;
}
export const getListDiff = (
prevList: ListData[] | undefined | null,
nextList: ListData[] | undefined | null
nextList: ListData[] | undefined | null,
options: ListOptions = { showOnly: [] }
): ListDiff => {
if (!prevList && !nextList) {
return {
type: "list",
status: STATUS.EQUAL,
status: LIST_STATUS.EQUAL,
diff: [],
};
}
if (!prevList) {
return formatSingleListDiff(nextList as ListData, STATUS.ADDED);
return formatSingleListDiff(
nextList as ListData,
LIST_STATUS.ADDED,
options
);
}
if (!nextList) {
return formatSingleListDiff(prevList as ListData, STATUS.DELETED);
return formatSingleListDiff(
prevList as ListData,
LIST_STATUS.DELETED,
options
);
}
const diff: ListDiff["diff"] = [];
const prevIndexMatches: number[] = [];
@ -58,7 +89,7 @@ export const getListDiff = ( @@ -58,7 +89,7 @@ export const getListDiff = (
prevIndex,
newIndex: i,
indexDiff,
status: STATUS.EQUAL,
status: LIST_STATUS.EQUAL,
});
}
if (prevIndex === -1) {
@ -67,7 +98,7 @@ export const getListDiff = ( @@ -67,7 +98,7 @@ export const getListDiff = (
prevIndex: null,
newIndex: i,
indexDiff,
status: STATUS.ADDED,
status: LIST_STATUS.ADDED,
});
}
return diff.push({
@ -75,7 +106,7 @@ export const getListDiff = ( @@ -75,7 +106,7 @@ export const getListDiff = (
prevIndex,
newIndex: i,
indexDiff,
status: STATUS.MOVED,
status: LIST_STATUS.MOVED,
});
});
@ -86,10 +117,17 @@ export const getListDiff = ( @@ -86,10 +117,17 @@ export const getListDiff = (
prevIndex: i,
newIndex: null,
indexDiff: null,
status: STATUS.DELETED,
status: LIST_STATUS.DELETED,
});
}
});
if (options.showOnly && options?.showOnly?.length > 0) {
return {
type: "list",
status: getListStatus(diff),
diff: getLeanDiff(diff, options.showOnly),
};
}
return {
type: "list",
status: getListStatus(diff),

70
src/model.ts

@ -1,45 +1,89 @@ @@ -1,45 +1,89 @@
export const STATUS: Record<string, DiffStatus> = {
export const STATUS: Record<string, ObjectDiffStatus> = {
ADDED: "added",
EQUAL: "equal",
MOVED: "moved",
DELETED: "deleted",
UPDATED: "updated",
};
export type DiffStatus = "added" | "equal" | "moved" | "deleted" | "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 Options = { discardArrayOrder?: boolean };
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]>;
};
export type ListDiff = {
type: "list";
status: DiffStatus;
status: ListDiffStatus;
diff: {
value: ListData;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: DiffStatus;
status: ListDiffStatus;
}[];
};
export type Subproperties = {
name: string;
export type SubProperties = {
property: string;
previousValue: any;
currentValue: any;
status: DiffStatus;
subDiff?: Subproperties[];
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
};
export type ObjectDiff = {
type: "object";
status: DiffStatus;
status: ObjectDiffStatus;
diff: {
property: string;
previousValue: any;
currentValue: any;
status: DiffStatus;
subPropertiesDiff?: Subproperties[];
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
}[];
};

112
src/object-diff.ts

@ -1,14 +1,51 @@ @@ -1,14 +1,51 @@
import {
GRANULARITY,
STATUS,
ObjectData,
ObjectDiff,
DiffStatus,
STATUS,
Subproperties,
Options,
ObjectDiffStatus,
ObjectOptions,
SubProperties,
} from "./model";
import { isObject, isEqual } from "./utils";
import { isEqual, isObject } from "./utils";
function getLeanDiff(
diff: ObjectDiff["diff"],
showOnly: ObjectOptions["showOnly"] = {
statuses: [],
granularity: GRANULARITY.BASIC,
}
): ObjectDiff["diff"] {
const { statuses, granularity } = showOnly;
return diff.reduce((acc, value) => {
if (granularity === GRANULARITY.DEEP && value.subPropertiesDiff) {
const cleanSubPropertiesDiff = getLeanDiff(
value.subPropertiesDiff,
showOnly
);
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)) {
return [...acc, value];
}
return acc;
}, [] as ObjectDiff["diff"]);
}
function getObjectStatus(diff: ObjectDiff["diff"]): DiffStatus {
function getObjectStatus(diff: ObjectDiff["diff"]): ObjectDiffStatus {
return diff.some((property) => property.status !== STATUS.EQUAL)
? STATUS.UPDATED
: STATUS.EQUAL;
@ -16,22 +53,26 @@ function getObjectStatus(diff: ObjectDiff["diff"]): DiffStatus { @@ -16,22 +53,26 @@ function getObjectStatus(diff: ObjectDiff["diff"]): DiffStatus {
function formatSingleObjectDiff(
data: ObjectData,
status: DiffStatus
status: ObjectDiffStatus,
options: ObjectOptions = {
ignoreArrayOrder: false,
showOnly: { statuses: [], granularity: GRANULARITY.BASIC },
}
): ObjectDiff {
if (!data) {
return {
type: "object",
status: STATUS.isEqual,
status: STATUS.EQUAL,
diff: [],
};
}
const diff: ObjectDiff["diff"] = [];
Object.entries(data).forEach(([property, value]) => {
if (isObject(value)) {
const subPropertiesDiff: Subproperties[] = [];
const subPropertiesDiff: SubProperties[] = [];
Object.entries(value).forEach(([subProperty, subValue]) => {
subPropertiesDiff.push({
name: subProperty,
property: subProperty,
previousValue: status === STATUS.ADDED ? undefined : subValue,
currentValue: status === STATUS.ADDED ? subValue : undefined,
status,
@ -52,6 +93,13 @@ function formatSingleObjectDiff( @@ -52,6 +93,13 @@ function formatSingleObjectDiff(
status,
});
});
if (options.showOnly && options.showOnly.statuses.length > 0) {
return {
type: "object",
status,
diff: getLeanDiff(diff, options.showOnly),
};
}
return {
type: "object",
status,
@ -62,7 +110,7 @@ function formatSingleObjectDiff( @@ -62,7 +110,7 @@ function formatSingleObjectDiff(
function getPreviousMatch(
previousValue: any | undefined,
nextSubProperty: any,
options?: Options
options?: ObjectOptions
): any | undefined {
if (!previousValue) {
return undefined;
@ -76,15 +124,17 @@ function getPreviousMatch( @@ -76,15 +124,17 @@ function getPreviousMatch(
function getValueStatus(
previousValue: any,
nextValue: any,
options?: Options
): DiffStatus {
options?: ObjectOptions
): ObjectDiffStatus {
if (isEqual(previousValue, nextValue, options)) {
return STATUS.EQUAL;
}
return STATUS.UPDATED;
}
function getPropertyStatus(subPropertiesDiff: Subproperties[]): DiffStatus {
function getPropertyStatus(
subPropertiesDiff: SubProperties[]
): ObjectDiffStatus {
return subPropertiesDiff.some((property) => property.status !== STATUS.EQUAL)
? STATUS.UPDATED
: STATUS.EQUAL;
@ -110,10 +160,10 @@ function getDeletedProperties( @@ -110,10 +160,10 @@ function getDeletedProperties(
function getSubPropertiesDiff(
previousValue: Record<string, any> | undefined,
nextValue: Record<string, any>,
options?: Options
): Subproperties[] {
const subPropertiesDiff: Subproperties[] = [];
let subDiff: Subproperties[];
options?: ObjectOptions
): SubProperties[] {
const subPropertiesDiff: SubProperties[] = [];
let subDiff: SubProperties[];
const deletedMainSubProperties = getDeletedProperties(
previousValue,
nextValue
@ -121,7 +171,7 @@ function getSubPropertiesDiff( @@ -121,7 +171,7 @@ function getSubPropertiesDiff(
if (deletedMainSubProperties) {
deletedMainSubProperties.forEach((deletedProperty) => {
subPropertiesDiff.push({
name: deletedProperty.property,
property: deletedProperty.property,
previousValue: deletedProperty.value,
currentValue: undefined,
status: STATUS.DELETED,
@ -136,7 +186,7 @@ function getSubPropertiesDiff( @@ -136,7 +186,7 @@ function getSubPropertiesDiff(
);
if (!!!previousMatch) {
return subPropertiesDiff.push({
name: nextSubProperty,
property: nextSubProperty,
previousValue: previousMatch,
currentValue: nextSubValue,
status:
@ -148,7 +198,7 @@ function getSubPropertiesDiff( @@ -148,7 +198,7 @@ function getSubPropertiesDiff(
});
}
if (isObject(nextSubValue)) {
const data: Subproperties[] = getSubPropertiesDiff(
const data: SubProperties[] = getSubPropertiesDiff(
previousMatch,
nextSubValue,
options
@ -159,7 +209,7 @@ function getSubPropertiesDiff( @@ -159,7 +209,7 @@ function getSubPropertiesDiff(
}
if (previousMatch) {
subPropertiesDiff.push({
name: nextSubProperty,
property: nextSubProperty,
previousValue: previousMatch,
currentValue: nextSubValue,
status: getValueStatus(previousMatch, nextSubValue, options),
@ -173,7 +223,10 @@ function getSubPropertiesDiff( @@ -173,7 +223,10 @@ function getSubPropertiesDiff(
export function getObjectDiff(
prevData: ObjectData,
nextData: ObjectData,
options?: Options
options: ObjectOptions = {
ignoreArrayOrder: false,
showOnly: { statuses: [], granularity: GRANULARITY.BASIC },
}
): ObjectDiff {
if (!prevData && !nextData) {
return {
@ -183,10 +236,10 @@ export function getObjectDiff( @@ -183,10 +236,10 @@ export function getObjectDiff(
};
}
if (!prevData) {
return formatSingleObjectDiff(nextData, STATUS.ADDED);
return formatSingleObjectDiff(nextData, STATUS.ADDED, options);
}
if (!nextData) {
return formatSingleObjectDiff(prevData, STATUS.DELETED);
return formatSingleObjectDiff(prevData, STATUS.DELETED, options);
}
const diff: ObjectDiff["diff"] = [];
Object.entries(nextData).forEach(([nextProperty, nextValue]) => {
@ -204,7 +257,7 @@ export function getObjectDiff( @@ -204,7 +257,7 @@ export function getObjectDiff(
});
}
if (isObject(nextValue)) {
const subPropertiesDiff: Subproperties[] = getSubPropertiesDiff(
const subPropertiesDiff: SubProperties[] = getSubPropertiesDiff(
previousValue,
nextValue,
options
@ -236,6 +289,13 @@ export function getObjectDiff( @@ -236,6 +289,13 @@ export function getObjectDiff(
});
});
}
if (options.showOnly && options.showOnly.statuses.length > 0) {
return {
type: "object",
status: getObjectStatus(diff),
diff: getLeanDiff(diff, options.showOnly),
};
}
return {
type: "object",
status: getObjectStatus(diff),

6
src/utils.ts

@ -1,16 +1,16 @@ @@ -1,16 +1,16 @@
import { Options } from "./model";
import { isEqualOptions } from "./model";
export function isEqual(
a: any,
b: any,
options: Options = { discardArrayOrder: false }
options: isEqualOptions = { ignoreArrayOrder: false }
): boolean {
if (typeof a !== typeof b) return false;
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
if (options.discardArrayOrder) {
if (options.ignoreArrayOrder) {
return a.every((v) =>
b.some((nextV: any) => JSON.stringify(nextV) === JSON.stringify(v))
);

116
test/list-diff.test.ts

@ -393,4 +393,120 @@ describe("getListDiff", () => { @@ -393,4 +393,120 @@ describe("getListDiff", () => {
],
});
});
it("showOnly added and deleted values", () => {
expect(
getListDiff(
[
false,
true,
true,
undefined,
"hello",
{ name: "joe", age: 88 },
false,
13,
],
[
false,
false,
true,
undefined,
"hello",
{ name: "joe", age: 88 },
false,
{ name: "joe", age: 88 },
],
{ showOnly: ["added", "deleted"] }
)
).toStrictEqual({
type: "list",
status: "updated",
diff: [
{
value: true,
prevIndex: 2,
newIndex: null,
indexDiff: null,
status: "deleted",
},
{
value: 13,
prevIndex: 7,
newIndex: null,
indexDiff: null,
status: "deleted",
},
{
value: false,
prevIndex: null,
newIndex: 6,
indexDiff: null,
status: "added",
},
{
value: { name: "joe", age: 88 },
prevIndex: null,
newIndex: 7,
indexDiff: null,
status: "added",
},
],
});
});
it("returns an empty diff if no property match the required statuses output", () => {
expect(getListDiff(null, null)).toStrictEqual({
type: "list",
status: "equal",
diff: [],
});
expect(
getListDiff(["mbappe", "mendes", "verratti", "ruiz"], null, {
showOnly: ["moved", "updated"],
})
).toStrictEqual({
type: "list",
status: "deleted",
diff: [],
});
});
it("returns all values if their status match the required statuses", () => {
expect(
getListDiff(null, ["mbappe", "mendes", "verratti", "ruiz"], {
showOnly: ["added"],
})
).toStrictEqual({
type: "list",
status: "added",
diff: [
{
value: "mbappe",
prevIndex: null,
newIndex: 0,
indexDiff: null,
status: "added",
},
{
value: "mendes",
prevIndex: null,
newIndex: 1,
indexDiff: null,
status: "added",
},
{
value: "verratti",
prevIndex: null,
newIndex: 2,
indexDiff: null,
status: "added",
},
{
value: "ruiz",
prevIndex: null,
newIndex: 3,
indexDiff: null,
status: "added",
},
],
});
});
});

434
test/object-diff.test.ts

@ -191,31 +191,31 @@ describe("getObjectDiff", () => { @@ -191,31 +191,31 @@ describe("getObjectDiff", () => {
status: "updated",
subPropertiesDiff: [
{
name: "age",
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{
name: "name",
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
{
name: "member",
property: "member",
previousValue: true,
currentValue: false,
status: "updated",
},
{
name: "hobbies",
property: "hobbies",
previousValue: ["golf", "football"],
currentValue: ["golf", "chess"],
status: "updated",
},
{
name: "nickname",
property: "nickname",
previousValue: undefined,
currentValue: "super joe",
status: "added",
@ -296,13 +296,13 @@ describe("getObjectDiff", () => { @@ -296,13 +296,13 @@ describe("getObjectDiff", () => {
status: "updated",
subPropertiesDiff: [
{
name: "name",
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
{
name: "data",
property: "data",
previousValue: {
member: true,
hobbies: {
@ -320,13 +320,13 @@ describe("getObjectDiff", () => { @@ -320,13 +320,13 @@ describe("getObjectDiff", () => {
status: "updated",
subDiff: [
{
name: "member",
property: "member",
previousValue: true,
currentValue: true,
status: "equal",
},
{
name: "hobbies",
property: "hobbies",
previousValue: {
football: ["psg"],
rugby: ["france"],
@ -338,19 +338,19 @@ describe("getObjectDiff", () => { @@ -338,19 +338,19 @@ describe("getObjectDiff", () => {
status: "updated",
subDiff: [
{
name: "rugby",
property: "rugby",
previousValue: ["france"],
currentValue: undefined,
status: "deleted",
},
{
name: "football",
property: "football",
previousValue: ["psg"],
currentValue: ["psg", "nantes"],
status: "updated",
},
{
name: "golf",
property: "golf",
previousValue: undefined,
currentValue: ["st andrews"],
status: "added",
@ -364,7 +364,7 @@ describe("getObjectDiff", () => { @@ -364,7 +364,7 @@ describe("getObjectDiff", () => {
],
});
});
it("detects changed between two objects BUT doesn't care about array order as long as all values are preserved when discardArrayOrder option is activated", () => {
it("detects changed between two objects BUT doesn't care about array order as long as all values are preserved when ignoreArrayOrder option is activated", () => {
expect(
getObjectDiff(
{
@ -387,7 +387,7 @@ describe("getObjectDiff", () => { @@ -387,7 +387,7 @@ describe("getObjectDiff", () => {
nickname: "super joe",
},
},
{ discardArrayOrder: true }
{ ignoreArrayOrder: true }
)
).toStrictEqual({
type: "object",
@ -422,31 +422,31 @@ describe("getObjectDiff", () => { @@ -422,31 +422,31 @@ describe("getObjectDiff", () => {
status: "updated",
subPropertiesDiff: [
{
name: "age",
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{
name: "name",
property: "name",
previousValue: "joe",
currentValue: "joe",
status: "equal",
},
{
name: "member",
property: "member",
previousValue: true,
currentValue: false,
status: "updated",
},
{
name: "hobbies",
property: "hobbies",
previousValue: ["golf", "football"],
currentValue: ["football", "golf"],
status: "equal",
},
{
name: "nickname",
property: "nickname",
previousValue: undefined,
currentValue: "super joe",
status: "added",
@ -462,4 +462,400 @@ describe("getObjectDiff", () => { @@ -462,4 +462,400 @@ describe("getObjectDiff", () => {
],
});
});
it("shows only main added values", () => {
expect(
getObjectDiff(
{
id: 54,
type: "sport",
user: {
name: "joe",
member: true,
hobbies: ["golf", "football"],
age: 66,
},
},
{
id: 54,
country: "us",
user: {
name: "joe",
member: false,
hobbies: ["golf", "chess"],
nickname: "super joe",
},
},
{ showOnly: { statuses: ["added"] } }
)
).toStrictEqual({
type: "object",
status: "updated",
diff: [
{
property: "country",
previousValue: undefined,
currentValue: "us",
status: "added",
},
],
});
});
it("shows only added and deleted values in nested objects", () => {
expect(
getObjectDiff(
{
id: 54,
type: "sport",
user: {
name: "joe",
member: true,
hobbies: ["golf", "football"],
age: 66,
},
},
{
id: 54,
country: "us",
user: {
name: "joe",
member: false,
hobbies: ["golf", "chess"],
nickname: "super joe",
},
},
{ showOnly: { statuses: ["added", "deleted"], granularity: "deep" } }
)
).toStrictEqual({
type: "object",
status: "updated",
diff: [
{
property: "country",
previousValue: undefined,
currentValue: "us",
status: "added",
},
{
property: "user",
previousValue: {
name: "joe",
member: true,
hobbies: ["golf", "football"],
age: 66,
},
currentValue: {
name: "joe",
member: false,
hobbies: ["golf", "chess"],
nickname: "super joe",
},
status: "updated",
subPropertiesDiff: [
{
property: "age",
previousValue: 66,
currentValue: undefined,
status: "deleted",
},
{
property: "nickname",
previousValue: undefined,
currentValue: "super joe",
status: "added",
},
],
},
{
property: "type",
previousValue: "sport",
currentValue: undefined,
status: "deleted",
},
],
});
});
it("shows only updated values in deeply nested objects", () => {
expect(
getObjectDiff(
{
id: 54,
user: {
name: "joe",
data: {
member: true,
hobbies: {
football: ["psg"],
rugby: ["france"],
},
},
},
},
{
id: 54,
user: {
name: "joe",
data: {
member: true,
hobbies: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
},
},
},
{
showOnly: {
statuses: ["updated"],
granularity: "deep",
},
}
)
).toStrictEqual({
type: "object",
status: "updated",
diff: [
{
property: "user",
previousValue: {
name: "joe",
data: {
member: true,
hobbies: {
football: ["psg"],
rugby: ["france"],
},
},
},
currentValue: {
name: "joe",
data: {
member: true,
hobbies: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
},
},
status: "updated",
subPropertiesDiff: [
{
property: "data",
previousValue: {
member: true,
hobbies: {
football: ["psg"],
rugby: ["france"],
},
},
currentValue: {
member: true,
hobbies: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
},
status: "updated",
subDiff: [
{
property: "hobbies",
previousValue: {
football: ["psg"],
rugby: ["france"],
},
currentValue: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
status: "updated",
subDiff: [
{
property: "football",
previousValue: ["psg"],
currentValue: ["psg", "nantes"],
status: "updated",
},
],
},
],
},
],
},
],
});
});
it("shows only added values in deeply nested objects", () => {
expect(
getObjectDiff(
{
id: 54,
user: {
name: "joe",
data: {
member: true,
hobbies: {
rugby: ["france"],
},
},
},
},
{
id: 54,
user: {
name: "joe",
data: {
member: true,
hobbies: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
},
},
},
{
showOnly: {
statuses: ["added"],
granularity: "deep",
},
}
)
).toStrictEqual({
type: "object",
status: "updated",
diff: [
{
property: "user",
previousValue: {
name: "joe",
data: {
member: true,
hobbies: {
rugby: ["france"],
},
},
},
currentValue: {
name: "joe",
data: {
member: true,
hobbies: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
},
},
status: "updated",
subPropertiesDiff: [
{
property: "data",
previousValue: {
member: true,
hobbies: {
rugby: ["france"],
},
},
currentValue: {
member: true,
hobbies: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
},
status: "updated",
subDiff: [
{
property: "hobbies",
previousValue: {
rugby: ["france"],
},
currentValue: {
football: ["psg", "nantes"],
golf: ["st andrews"],
},
status: "updated",
subDiff: [
{
property: "football",
previousValue: undefined,
currentValue: ["psg", "nantes"],
status: "added",
},
{
property: "golf",
previousValue: undefined,
currentValue: ["st andrews"],
status: "added",
},
],
},
],
},
],
},
],
});
});
it("returns an empty diff if no property match the required statuses output", () => {
expect(
getObjectDiff(
null,
{
name: "joe",
age: 54,
hobbies: ["golf", "football"],
},
{ showOnly: { statuses: ["deleted"], granularity: "deep" } }
)
).toStrictEqual({
type: "object",
status: "added",
diff: [],
});
});
expect(
getObjectDiff(
{
name: "joe",
age: 54,
hobbies: ["golf", "football"],
},
null,
{ showOnly: { statuses: ["added"], granularity: "deep" } }
)
).toStrictEqual({
type: "object",
status: "deleted",
diff: [],
});
it("returns all values if their status match the required statuses", () => {
expect(
getObjectDiff(
{ name: "joe", age: 54, hobbies: ["golf", "football"] },
null,
{ showOnly: { statuses: ["deleted"] } }
)
).toStrictEqual({
type: "object",
status: "deleted",
diff: [
{
property: "name",
previousValue: "joe",
currentValue: undefined,
status: "deleted",
},
{
property: "age",
previousValue: 54,
currentValue: undefined,
status: "deleted",
},
{
property: "hobbies",
previousValue: ["golf", "football"],
currentValue: undefined,
status: "deleted",
},
],
});
});
});

20
test/utils.test.ts

@ -40,14 +40,14 @@ describe("isEqual", () => { @@ -40,14 +40,14 @@ describe("isEqual", () => {
).toBeFalsy();
expect(isEqual(["psg"], ["psg", "nantes"])).toBeFalsy();
});
it("return true if discardArrayOrder option is activated and arrays contains the same values regardless of their positions", () => {
it("return true if ignoreArrayOrder option is activated and arrays contains the same values regardless of their positions", () => {
expect(
isEqual(["hello", "world"], ["world", "hello"], {
discardArrayOrder: true,
ignoreArrayOrder: true,
})
).toBeTruthy();
expect(
isEqual([44, 45, "world"], [45, "world", 44], { discardArrayOrder: true })
isEqual([44, 45, "world"], [45, "world", 44], { ignoreArrayOrder: true })
).toBeTruthy();
expect(
isEqual(
@ -60,24 +60,24 @@ describe("isEqual", () => { @@ -60,24 +60,24 @@ describe("isEqual", () => {
{ name: "joe", age: 88 },
],
{
discardArrayOrder: true,
ignoreArrayOrder: true,
}
)
).toBeTruthy();
expect(
isEqual([true, 55, "hello"], ["hello", 55, true], {
discardArrayOrder: true,
ignoreArrayOrder: true,
})
).toBeTruthy();
});
it("return false if discardArrayOrder option is activated but the arrays don't contain the same values", () => {
it("return false if ignoreArrayOrder option is activated but the arrays don't contain the same values", () => {
expect(
isEqual(["hello"], ["world", "hello"], {
discardArrayOrder: true,
ignoreArrayOrder: true,
})
).toBeFalsy();
expect(
isEqual([44, 47, "world"], [45, "world", 44], { discardArrayOrder: true })
isEqual([44, 47, "world"], [45, "world", 44], { ignoreArrayOrder: true })
).toBeFalsy();
expect(
isEqual(
@ -90,13 +90,13 @@ describe("isEqual", () => { @@ -90,13 +90,13 @@ describe("isEqual", () => {
{ name: "joe", age: 88 },
],
{
discardArrayOrder: true,
ignoreArrayOrder: true,
}
)
).toBeFalsy();
expect(
isEqual([false, 55, "hello"], ["hello", 55, true], {
discardArrayOrder: true,
ignoreArrayOrder: true,
})
).toBeFalsy();
});

1056
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save