Browse Source

feat: allow referenceProperty for object comparison in lists

pull/16/head
DoneDeal0 1 year ago
parent
commit
fd721c5953
  1. 34
      README.md
  2. 93
      dist/index.d.mts
  3. 28
      dist/index.d.ts
  4. 10
      dist/index.js
  5. 4
      dist/index.mjs
  6. 7721
      package-lock.json
  7. 12
      package.json
  8. 70
      src/list-diff.ts
  9. 3
      src/model.ts
  10. 6
      src/object-diff.ts
  11. 11
      src/utils.ts
  12. 78
      test/list-diff.test.ts
  13. 3508
      yarn.lock

34
README.md

@ -2,15 +2,15 @@ @@ -2,15 +2,15 @@
# SUPERDIFF
This library compares two arrays or objects and return a complete diff of their differences.
This library compares two arrays or objects and returns a full diff of their differences.
[![Superdiff CI](https://github.com/DoneDeal0/superdiff/actions/workflows/superdiff.yml/badge.svg)](https://github.com/DoneDeal0/superdiff/actions/workflows/superdiff.yml)
## WHY YOU SHOULD USE THIS LIB
## WHY YOU SHOULD USE THIS LIBRARY
All other existing solutions return a weird diff format which often require an 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 with a very readable format. Last but not least, it's battled 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. 👍
## DIFF FORMAT COMPARISON
@ -173,13 +173,13 @@ You can add a third `options` parameter to `getObjectDiff`. @@ -173,13 +173,13 @@ You can add a third `options` parameter to `getObjectDiff`.
}
```
- `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:
- `ignoreArrayOrder`: if set to `true`, `["hello", "world"]` and `["world", "hello"]` will be treated as `equal`, because the two arrays have the same value, just not in the same order.
- `showOnly`: returns only the values whose status you are interested in. It takes two parameters:
- `statuses`: status you want to see in the output (ex: `["added", "equal"]`)
- `statuses`: status you want to see in the output (e.g. `["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.
- `basic` returns only the main properties whose status matches your query.
- `deep` can return main properties if some of their subproperties' status match your request. The subproperties are filtered accordingly.
### getListDiff()
@ -187,13 +187,13 @@ You can add a third `options` parameter to `getObjectDiff`. @@ -187,13 +187,13 @@ You can add a third `options` parameter to `getObjectDiff`.
import { getListDiff } from "@donedeal0/superdiff";
```
Compares two arrays and return a diff for each value:
Compares two arrays and returns a diff for each value:
- index change: `prevIndex`, `newIndex`, `indexDiff`
- status: `added`, `deleted`, `equal`, `moved`, `updated`
- value
- supports arrays of primitive values and objects
- supports arrays with duplicated values
- supports arrays with duplicate values
format:
@ -218,10 +218,12 @@ You can add a third `options` parameter to `getListDiff`. @@ -218,10 +218,12 @@ You can add a third `options` parameter to `getListDiff`.
```ts
{
showOnly?: ("added" | "deleted" | "moved" | "updated" | "equal")[], // [] by default
referenceProperty?: string; // "" by default
}
```
- `showOnly` gives you the option to only return the values whose status interest you (ex: `["added", "equal"]`).
- `showOnly` gives you the option to return only the values whose status you are interested in (e.g. `["added", "equal"]`).
- `referenceProperty` will consider an object to be updated instead of added or deleted if one of its properties remains stable, such as its `id`. This option has no effect on other datatypes.
### isEqual()
@ -229,7 +231,7 @@ You can add a third `options` parameter to `getListDiff`. @@ -229,7 +231,7 @@ You can add a third `options` parameter to `getListDiff`.
import { isEqual } from "@donedeal0/superdiff";
```
Checks if two values are equal.
Tests whether two values are equal.
**Options**
@ -241,7 +243,7 @@ You can add a third `options` parameter to `isEqual`. @@ -241,7 +243,7 @@ You can add a third `options` parameter to `isEqual`.
}
```
- `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.
- `ignoreArrayOrder`: if set to `true`, `["hello", "world"]` and `["world", "hello"]` will be treated as `equal`, because the two arrays have the same value, just not in the same order.
### isObject()
@ -249,7 +251,7 @@ You can add a third `options` parameter to `isEqual`. @@ -249,7 +251,7 @@ You can add a third `options` parameter to `isEqual`.
import { isObject } from "@donedeal0/superdiff";
```
Checks if a value is an object.
Tests whether a value is an object.
## EXAMPLES
@ -431,7 +433,7 @@ output @@ -431,7 +433,7 @@ output
false;
```
More examples are available in the tests of the source code.
More examples are available in the source code tests.
<hr/>

93
dist/index.d.mts vendored

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
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 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]>;
referenceProperty?: string;
};
type ListDiff = {
type: "list";
status: ListDiffStatus;
diff: {
value: ListData;
prevIndex: number | null;
newIndex: number | null;
indexDiff: number | null;
status: ListDiffStatus;
}[];
};
type SubProperties = {
property: string;
previousValue: any;
currentValue: any;
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
};
type ObjectDiff = {
type: "object";
status: ObjectDiffStatus;
diff: {
property: string;
previousValue: any;
currentValue: any;
status: ObjectDiffStatus;
subPropertiesDiff?: SubProperties[];
}[];
};
/**
* Returns the diff between two objects
* @param {Record<string, any>} prevData - The original object.
* @param {Record<string, any>} nextData - The new object.
* @returns ObjectDiff
*/
declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: ObjectOptions): ObjectDiff;
/**
* Returns the diff between two arrays
* @param {Array<T>} prevList - The original array.
* @param {Array<T>} nextList - The new array.
* @returns ListDiff
*/
declare const getListDiff: <T>(prevList: T[] | null | undefined, nextList: T[] | null | undefined, options?: ListOptions) => ListDiff;
/**
* Returns true if two data are equal
* @param {any} a - The original data.
* @param {any} b- The data to compare.
* @returns boolean
*/
declare function isEqual(a: any, b: any, options?: isEqualOptions): boolean;
/**
* Returns true if the provided value is an object
* @param {any} value - The data to check.
* @returns value is Record<string, any>
*/
declare function isObject(value: any): value is Record<string, any>;
export { getListDiff, getObjectDiff, isEqual, isObject };

28
dist/index.d.ts vendored

@ -23,11 +23,12 @@ type ObjectOptions = { @@ -23,11 +23,12 @@ type ObjectOptions = {
ignoreArrayOrder?: boolean;
showOnly?: {
statuses: Array<ObjectStatusTuple[number]>;
granularity?: typeof GRANULARITY[keyof typeof GRANULARITY];
granularity?: (typeof GRANULARITY)[keyof typeof GRANULARITY];
};
};
type ListOptions = {
showOnly?: Array<ListStatusTuple[number]>;
referenceProperty?: string;
};
type ListDiff = {
type: "list";
@ -59,11 +60,34 @@ type ObjectDiff = { @@ -59,11 +60,34 @@ type ObjectDiff = {
}[];
};
/**
* Returns the diff between two objects
* @param {Record<string, any>} prevData - The original object.
* @param {Record<string, any>} nextData - The new object.
* @returns ObjectDiff
*/
declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: ObjectOptions): ObjectDiff;
declare const getListDiff: (prevList: ListData[] | undefined | null, nextList: ListData[] | undefined | null, options?: ListOptions) => ListDiff;
/**
* Returns the diff between two arrays
* @param {Array<T>} prevList - The original array.
* @param {Array<T>} nextList - The new array.
* @returns ListDiff
*/
declare const getListDiff: <T>(prevList: T[] | null | undefined, nextList: T[] | null | undefined, options?: ListOptions) => ListDiff;
/**
* Returns true if two data are equal
* @param {any} a - The original data.
* @param {any} b- The data to compare.
* @returns boolean
*/
declare function isEqual(a: any, b: any, options?: isEqualOptions): boolean;
/**
* Returns true if the provided value is an object
* @param {any} value - The data to check.
* @returns value is Record<string, any>
*/
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 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}};
var u={ADDED:"added",EQUAL:"equal",DELETED:"deleted",UPDATED:"updated"},d={...u,MOVED:"moved"},y={BASIC:"basic",DEEP:"deep"};function a(e,t,f={ignoreArrayOrder:!1}){return typeof e!=typeof t?!1:Array.isArray(e)?e.length!==t.length?!1:f.ignoreArrayOrder?e.every(i=>t.some(n=>JSON.stringify(n)===JSON.stringify(i))):e.every((i,n)=>JSON.stringify(i)===JSON.stringify(t[n])):typeof e=="object"?JSON.stringify(e)===JSON.stringify(t):e===t}function p(e){return !!e&&typeof e=="object"&&!Array.isArray(e)}function O(e,t={statuses:[],granularity:y.BASIC}){let{statuses:f,granularity:i}=t;return e.reduce((n,r)=>{if(i===y.DEEP&&r.subPropertiesDiff){let s=O(r.subPropertiesDiff,t);if(s.length>0)return [...n,{...r,subPropertiesDiff:s}]}if(i===y.DEEP&&r.subDiff){let s=O(r.subDiff,t);if(s.length>0)return [...n,{...r,subDiff:s}]}return f.includes(r.status)?[...n,r]:n},[])}function E(e){return e.some(t=>t.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function A(e,t,f={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:y.BASIC}}){if(!e)return {type:"object",status:u.EQUAL,diff:[]};let i=[];return Object.entries(e).forEach(([n,r])=>{if(p(r)){let s=[];return Object.entries(r).forEach(([o,c])=>{s.push({property:o,previousValue:t===u.ADDED?void 0:c,currentValue:t===u.ADDED?c:void 0,status:t});}),i.push({property:n,previousValue:t===u.ADDED?void 0:e[n],currentValue:t===u.ADDED?r:void 0,status:t,subPropertiesDiff:s})}return i.push({property:n,previousValue:t===u.ADDED?void 0:e[n],currentValue:t===u.ADDED?r:void 0,status:t})}),f.showOnly&&f.showOnly.statuses.length>0?{type:"object",status:t,diff:O(i,f.showOnly)}:{type:"object",status:t,diff:i}}function P(e,t,f){if(!e)return;let i=Object.entries(e).find(([n])=>a(n,t,f));return i?i[1]:void 0}function j(e,t,f){return a(e,t,f)?u.EQUAL:u.UPDATED}function U(e){return e.some(t=>t.status!==u.EQUAL)?u.UPDATED:u.EQUAL}function g(e,t){if(!e)return;let f=Object.keys(e),i=Object.keys(t),n=f.filter(r=>!i.includes(r));if(n.length>0)return n.map(r=>({property:r,value:e[r]}))}function S(e,t,f){let i=[],n,r=g(e,t);return r&&r.forEach(s=>{i.push({property:s.property,previousValue:s.value,currentValue:void 0,status:u.DELETED});}),Object.entries(t).forEach(([s,o])=>{let c=P(e,s,f);if(!c)return i.push({property:s,previousValue:c,currentValue:o,status:!e||!(s in e)?u.ADDED:c===o?u.EQUAL:u.UPDATED});if(p(o)){let D=S(c,o,f);D&&D.length>0&&(n=D);}c&&i.push({property:s,previousValue:c,currentValue:o,status:j(c,o,f),...!!n&&{subDiff:n}});}),i}function m(e,t,f={ignoreArrayOrder:!1,showOnly:{statuses:[],granularity:y.BASIC}}){if(!e&&!t)return {type:"object",status:u.EQUAL,diff:[]};if(!e)return A(t,u.ADDED,f);if(!t)return A(e,u.DELETED,f);let i=[];Object.entries(t).forEach(([r,s])=>{let o=e[r];if(!o)return i.push({property:r,previousValue:o,currentValue:s,status:r in e?o===s?u.EQUAL:u.UPDATED:u.ADDED});if(p(s)){let c=S(o,s,f),D=U(c);return i.push({property:r,previousValue:o,currentValue:s,status:D,...D!==u.EQUAL&&{subPropertiesDiff:c}})}return i.push({property:r,previousValue:o,currentValue:s,status:j(o,s,f)})});let n=g(e,t);return n&&n.forEach(r=>{i.push({property:r.property,previousValue:r.value,currentValue:void 0,status:u.DELETED});}),f.showOnly&&f.showOnly.statuses.length>0?{type:"object",status:E(i),diff:O(i,f.showOnly)}:{type:"object",status:E(i),diff:i}}function w(e,t=[]){return e.filter(f=>t?.includes(f.status))}function h(e,t,f={showOnly:[]}){let i=e.map((n,r)=>({value:n,prevIndex:t===d.ADDED?null:r,newIndex:t===d.ADDED?r:null,indexDiff:null,status:t}));return f.showOnly&&f.showOnly.length>0?{type:"list",status:t,diff:i.filter(n=>f.showOnly?.includes(n.status))}:{type:"list",status:t,diff:i}}function L(e){return e.some(t=>t.status!==d.EQUAL)?d.UPDATED:d.EQUAL}function T(e,t){return p(e)&&t?Object.hasOwn(e,t):!1}var I=(e,t,f={showOnly:[],referenceProperty:void 0})=>{if(!e&&!t)return {type:"list",status:d.EQUAL,diff:[]};if(!e)return h(t,d.ADDED,f);if(!t)return h(e,d.DELETED,f);let i=[],n=[];return t.forEach((r,s)=>{let o=e.findIndex((D,b)=>T(D,f.referenceProperty)?p(r)?a(D[f.referenceProperty],r[f.referenceProperty])&&!n.includes(b):!1:a(D,r)&&!n.includes(b));o>-1&&n.push(o);let c=o===-1?null:s-o;if(c===0){let D=d.EQUAL;return T(r,f.referenceProperty)&&(a(e[o],r)||(D=d.UPDATED)),i.push({value:r,prevIndex:o,newIndex:s,indexDiff:c,status:D})}return o===-1?i.push({value:r,prevIndex:null,newIndex:s,indexDiff:c,status:d.ADDED}):i.push({value:r,prevIndex:o,newIndex:s,indexDiff:c,status:d.MOVED})}),e.forEach((r,s)=>{if(!n.includes(s))return i.splice(s,0,{value:r,prevIndex:s,newIndex:null,indexDiff:null,status:d.DELETED})}),f.showOnly&&f?.showOnly?.length>0?{type:"list",status:L(i),diff:w(i,f.showOnly)}:{type:"list",status:L(i),diff:i}};
exports.getListDiff = w;
exports.getObjectDiff = U;
exports.isEqual = p;
exports.isObject = l;
exports.getListDiff = I;
exports.getObjectDiff = m;
exports.isEqual = a;
exports.isObject = p;

4
dist/index.mjs vendored

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

7721
package-lock.json generated

File diff suppressed because it is too large Load Diff

12
package.json

@ -36,11 +36,11 @@ @@ -36,11 +36,11 @@
"test": "jest"
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@types/jest": "^29.4.0",
"jest": "^29.4.2",
"ts-jest": "^29.0.5",
"tsup": "^6.6.0",
"typescript": "^4.9.5"
"@babel/preset-env": "^7.23.8",
"@types/jest": "^29.5.11",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"tsup": "^8.0.1",
"typescript": "^5.3.3"
}
}

70
src/list-diff.ts

@ -5,7 +5,7 @@ import { @@ -5,7 +5,7 @@ import {
ListDiffStatus,
ListOptions,
} from "./model";
import { isEqual } from "./utils";
import { isEqual, isObject } from "./utils";
function getLeanDiff(
diff: ListDiff["diff"],
@ -14,12 +14,12 @@ function getLeanDiff( @@ -14,12 +14,12 @@ function getLeanDiff(
return diff.filter((value) => showOnly?.includes(value.status));
}
function formatSingleListDiff(
listData: ListData[],
function formatSingleListDiff<T>(
listData: T[],
status: ListDiffStatus,
options: ListOptions = { showOnly: [] }
): ListDiff {
const diff = listData.map((data: ListData, i) => ({
const diff = listData.map((data, i) => ({
value: data,
prevIndex: status === LIST_STATUS.ADDED ? null : i,
newIndex: status === LIST_STATUS.ADDED ? i : null,
@ -46,10 +46,26 @@ function getListStatus(listDiff: ListDiff["diff"]): ListDiffStatus { @@ -46,10 +46,26 @@ function getListStatus(listDiff: ListDiff["diff"]): ListDiffStatus {
: LIST_STATUS.EQUAL;
}
export const getListDiff = (
prevList: ListData[] | undefined | null,
nextList: ListData[] | undefined | null,
options: ListOptions = { showOnly: [] }
function isReferencedObject(
value: any,
referenceProperty: ListOptions["referenceProperty"]
): value is Record<string, any> {
if (isObject(value) && !!referenceProperty) {
return Object.hasOwn(value, referenceProperty);
}
return false;
}
/**
* Returns the diff between two arrays
* @param {Array<T>} prevList - The original array.
* @param {Array<T>} nextList - The new array.
* @returns ListDiff
*/
export const getListDiff = <T>(
prevList: T[] | undefined | null,
nextList: T[] | undefined | null,
options: ListOptions = { showOnly: [], referenceProperty: undefined }
): ListDiff => {
if (!prevList && !nextList) {
return {
@ -59,37 +75,47 @@ export const getListDiff = ( @@ -59,37 +75,47 @@ export const getListDiff = (
};
}
if (!prevList) {
return formatSingleListDiff(
nextList as ListData,
LIST_STATUS.ADDED,
options
);
return formatSingleListDiff(nextList as T[], LIST_STATUS.ADDED, options);
}
if (!nextList) {
return formatSingleListDiff(
prevList as ListData,
LIST_STATUS.DELETED,
options
);
return formatSingleListDiff(prevList as T[], LIST_STATUS.DELETED, options);
}
const diff: ListDiff["diff"] = [];
const prevIndexMatches: number[] = [];
nextList.forEach((nextValue, i) => {
const prevIndex = prevList.findIndex(
(prevValue, prevIdx) =>
const prevIndex = prevList.findIndex((prevValue, prevIdx) => {
if (isReferencedObject(prevValue, options.referenceProperty)) {
if (isObject(nextValue)) {
return (
isEqual(
prevValue[options.referenceProperty as string],
nextValue[options.referenceProperty as string]
) && !prevIndexMatches.includes(prevIdx)
);
}
return false;
}
return (
isEqual(prevValue, nextValue) && !prevIndexMatches.includes(prevIdx)
);
);
});
if (prevIndex > -1) {
prevIndexMatches.push(prevIndex);
}
const indexDiff = prevIndex === -1 ? null : i - prevIndex;
if (indexDiff === 0) {
let nextStatus = LIST_STATUS.EQUAL;
if (isReferencedObject(nextValue, options.referenceProperty)) {
if (!isEqual(prevList[prevIndex], nextValue)) {
nextStatus = LIST_STATUS.UPDATED;
}
}
return diff.push({
value: nextValue,
prevIndex,
newIndex: i,
indexDiff,
status: LIST_STATUS.EQUAL,
status: nextStatus,
});
}
if (prevIndex === -1) {

3
src/model.ts

@ -47,12 +47,13 @@ export type ObjectOptions = { @@ -47,12 +47,13 @@ export type ObjectOptions = {
ignoreArrayOrder?: boolean;
showOnly?: {
statuses: Array<ObjectStatusTuple[number]>;
granularity?: typeof GRANULARITY[keyof typeof GRANULARITY];
granularity?: (typeof GRANULARITY)[keyof typeof GRANULARITY];
};
};
export type ListOptions = {
showOnly?: Array<ListStatusTuple[number]>;
referenceProperty?: string;
};
export type ListDiff = {

6
src/object-diff.ts

@ -220,6 +220,12 @@ function getSubPropertiesDiff( @@ -220,6 +220,12 @@ function getSubPropertiesDiff(
return subPropertiesDiff;
}
/**
* Returns the diff between two objects
* @param {Record<string, any>} prevData - The original object.
* @param {Record<string, any>} nextData - The new object.
* @returns ObjectDiff
*/
export function getObjectDiff(
prevData: ObjectData,
nextData: ObjectData,

11
src/utils.ts

@ -1,5 +1,11 @@ @@ -1,5 +1,11 @@
import { isEqualOptions } from "./model";
/**
* Returns true if two data are equal
* @param {any} a - The original data.
* @param {any} b- The data to compare.
* @returns boolean
*/
export function isEqual(
a: any,
b: any,
@ -23,6 +29,11 @@ export function isEqual( @@ -23,6 +29,11 @@ export function isEqual(
return a === b;
}
/**
* Returns true if the provided value is an object
* @param {any} value - The data to check.
* @returns value is Record<string, any>
*/
export function isObject(value: any): value is Record<string, any> {
return !!value && typeof value === "object" && !Array.isArray(value);
}

78
test/list-diff.test.ts

@ -509,4 +509,82 @@ describe("getListDiff", () => { @@ -509,4 +509,82 @@ describe("getListDiff", () => {
],
});
});
it("consider object updated if a reference property is given and this property hasn't changed", () => {
expect(
getListDiff(
[
"hello",
{ id: 37, isCool: true, hobbies: ["golf", "ski"] },
{ id: 38, isCool: false, hobbies: ["football"] },
undefined,
{ id: 8, age: 77 },
{ id: 55, character: { strength: 66 } },
],
[
{ id: 8, age: 77 },
{ id: 37, isCool: false, hobbies: ["golf", "ski"] },
{ id: 38, isCool: false, hobbies: ["football"] },
undefined,
{ id: 99, character: { strength: 69 } },
],
{
referenceProperty: "id",
}
)
).toStrictEqual({
type: "list",
status: "updated",
diff: [
{
value: "hello",
prevIndex: 0,
newIndex: null,
indexDiff: null,
status: "deleted",
},
{
value: { id: 8, age: 77 },
prevIndex: 4,
newIndex: 0,
indexDiff: -4,
status: "moved",
},
{
value: { id: 37, isCool: false, hobbies: ["golf", "ski"] },
prevIndex: 1,
newIndex: 1,
indexDiff: 0,
status: "updated",
},
{
value: { id: 38, isCool: false, hobbies: ["football"] },
prevIndex: 2,
newIndex: 2,
indexDiff: 0,
status: "equal",
},
{
value: undefined,
prevIndex: 3,
newIndex: 3,
indexDiff: 0,
status: "equal",
},
{
value: { id: 55, character: { strength: 66 } },
prevIndex: 5,
newIndex: null,
indexDiff: null,
status: "deleted",
},
{
value: { id: 99, character: { strength: 69 } },
prevIndex: null,
newIndex: 4,
indexDiff: null,
status: "added",
},
],
});
});
});

3508
yarn.lock

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