diff --git a/README.md b/README.md index abab8ee..86bd840 100644 --- a/README.md +++ b/README.md @@ -97,25 +97,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", @@ -156,17 +156,37 @@ type ObjectDiff = { status: "added" | "deleted" | "equal" | "moved" | "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"; // subDiff is a recursive diff in case of nested subproperties - subDiff?: Subproperties[]; + subDiff?: SubProperties[]; }[]; }[]; }; ``` +**Options** + +```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`: gives you the option to only return 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, without taking into account their eventual subproperties. + - `deep` return main properties whose status match your request but also their relevant subproperties. + ### getListDiff() ```js @@ -197,6 +217,16 @@ type ListDiff = { }; ``` +**Options** + +```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 +235,16 @@ import { isEqual } from "@donedeal0/superdiff"; Checks if two values are equal. +**Options** + +```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 +369,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", @@ -397,25 +437,13 @@ More examples are availble in the tests of the source code.
-### 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 coffee: https://www.buymeacoffee.com/donedeal0
diff --git a/dist/index.d.ts b/dist/index.d.ts index ad1b53b..f4054d8 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -2,41 +2,48 @@ type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; type ObjectData = Record | undefined | null; type ListData = any; type Options = { - discardArrayOrder?: boolean; + ignoreArrayOrder?: boolean; }; type ListDiff = { - type: "list"; + type: "list"; + status: DiffStatus; + diff: { + value: ListData; + prevIndex: number | null; + newIndex: number | null; + indexDiff: number | null; status: DiffStatus; - diff: { - value: ListData; - prevIndex: number | null; - newIndex: number | null; - indexDiff: number | null; - status: DiffStatus; - }[]; + }[]; }; -type Subproperties = { - name: string; - previousValue: any; - currentValue: any; - status: DiffStatus; - subDiff?: Subproperties[]; +type SubProperties = { + name: string; + previousValue: any; + currentValue: any; + status: DiffStatus; + subDiff?: SubProperties[]; }; type ObjectDiff = { - type: "object"; + type: "object"; + status: DiffStatus; + diff: { + property: string; + previousValue: any; + currentValue: any; status: DiffStatus; - diff: { - property: string; - previousValue: any; - currentValue: any; - status: DiffStatus; - subPropertiesDiff?: Subproperties[]; - }[]; + subPropertiesDiff?: SubProperties[]; + }[]; }; -declare function getObjectDiff(prevData: ObjectData, nextData: ObjectData, options?: Options): ObjectDiff; +declare function getObjectDiff( + prevData: ObjectData, + nextData: ObjectData, + options?: Options +): ObjectDiff; -declare const getListDiff: (prevList: ListData[] | undefined | null, nextList: ListData[] | undefined | null) => ListDiff; +declare const getListDiff: ( + prevList: ListData[] | undefined | null, + nextList: ListData[] | undefined | null +) => ListDiff; declare function isEqual(a: any, b: any, options?: Options): boolean; declare function isObject(value: any): value is Record; diff --git a/dist/index.js b/dist/index.js index 6a83a40..8f96b27 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,6 +1,233 @@ -'use strict'; +"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 r = { + ADDED: "added", + EQUAL: "equal", + MOVED: "moved", + DELETED: "deleted", + UPDATED: "updated", +}; +function d(e, t, n = { ignoreArrayOrder: !1 }) { + return typeof e != typeof t + ? !1 + : Array.isArray(e) + ? e.length !== t.length + ? !1 + : n.ignoreArrayOrder + ? 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 } + ); +}; exports.getListDiff = m; exports.getObjectDiff = g; diff --git a/dist/index.mjs b/dist/index.mjs index eb15c8c..b08ad45 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -1,3 +1,230 @@ -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 r = { + ADDED: "added", + EQUAL: "equal", + MOVED: "moved", + DELETED: "deleted", + UPDATED: "updated", +}; +function d(e, t, n = { ignoreArrayOrder: !1 }) { + return typeof e != typeof t + ? !1 + : Array.isArray(e) + ? e.length !== t.length + ? !1 + : n.ignoreArrayOrder + ? 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 } + ); +}; export { h as getListDiff, L as getObjectDiff, d as isEqual, p as isObject }; diff --git a/src/list-diff.ts b/src/list-diff.ts index 9ca25c5..506bc57 100644 --- a/src/list-diff.ts +++ b/src/list-diff.ts @@ -1,45 +1,59 @@ -import { STATUS, ListDiff, ListData, DiffStatus } from "./model"; +import { + LIST_STATUS, + ListDiff, + ListData, + 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 ): ListDiff { return { type: "list", status, diff: listData.map((data: ListData, i) => ({ value: data, - prevIndex: status === STATUS.ADDED ? null : i, - newIndex: status === STATUS.ADDED ? i : null, + prevIndex: status === LIST_STATUS.ADDED ? null : i, + newIndex: status === LIST_STATUS.ADDED ? i : null, indexDiff: null, status, })), }; } -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); } if (!nextList) { - return formatSingleListDiff(prevList as ListData, STATUS.DELETED); + return formatSingleListDiff(prevList as ListData, LIST_STATUS.DELETED); } const diff: ListDiff["diff"] = []; const prevIndexMatches: number[] = []; @@ -58,7 +72,7 @@ export const getListDiff = ( prevIndex, newIndex: i, indexDiff, - status: STATUS.EQUAL, + status: LIST_STATUS.EQUAL, }); } if (prevIndex === -1) { @@ -67,7 +81,7 @@ export const getListDiff = ( prevIndex: null, newIndex: i, indexDiff, - status: STATUS.ADDED, + status: LIST_STATUS.ADDED, }); } return diff.push({ @@ -75,7 +89,7 @@ export const getListDiff = ( prevIndex, newIndex: i, indexDiff, - status: STATUS.MOVED, + status: LIST_STATUS.MOVED, }); }); @@ -86,10 +100,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), diff --git a/src/model.ts b/src/model.ts index 71bd953..b1101ba 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,45 +1,83 @@ -export const STATUS: Record = { +export const STATUS: Record = { ADDED: "added", EQUAL: "equal", - MOVED: "moved", DELETED: "deleted", UPDATED: "updated", }; -export type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated"; +export const LIST_STATUS: Record = { + ...STATUS, + MOVED: "moved", +}; + +export type ListDiffStatus = + | "added" + | "equal" + | "moved" + | "deleted" + | "updated"; +export type ObjectDiffStatus = "added" | "equal" | "deleted" | "updated"; export type ObjectData = Record | 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; + granularity?: "basic" | "deep"; + }; +}; + +export type ListOptions = { + showOnly?: Array; +}; 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[]; }[]; }; diff --git a/src/object-diff.ts b/src/object-diff.ts index eb224ef..3640705 100644 --- a/src/object-diff.ts +++ b/src/object-diff.ts @@ -1,14 +1,47 @@ import { ObjectData, ObjectDiff, - DiffStatus, STATUS, - Subproperties, - Options, + SubProperties, + ObjectOptions, + ObjectDiffStatus, } from "./model"; import { isObject, isEqual } from "./utils"; -function getObjectStatus(diff: ObjectDiff["diff"]): DiffStatus { +function getLeanDiff( + diff: ObjectDiff["diff"], + showOnly: ObjectOptions["showOnly"] = { statuses: [], granularity: "basic" } +): ObjectDiff["diff"] { + const { statuses, granularity } = showOnly; + return diff.reduce((acc, value) => { + if (statuses.includes(value.status)) { + return [...acc, value]; + } + if (granularity === "deep" && value.subPropertiesDiff) { + const cleanSubPropertiesDiff = getLeanDiff( + value.subPropertiesDiff, + showOnly + ); + if (cleanSubPropertiesDiff.length > 0) { + return [ + ...acc, + { ...value, subPropertiesDiff: cleanSubPropertiesDiff }, + ]; + } + } + // @ts-ignore + if (granularity === "deep" && value.subDiff) { + // @ts-ignore + const cleanSubDiff = getLeanDiff(value.subDiff, showOnly); + if (cleanSubDiff.length > 0) { + return [...acc, { ...value, subDiff: cleanSubDiff }]; + } + } + return acc; + }, [] as ObjectDiff["diff"]); +} + +function getObjectStatus(diff: ObjectDiff["diff"]): ObjectDiffStatus { return diff.some((property) => property.status !== STATUS.EQUAL) ? STATUS.UPDATED : STATUS.EQUAL; @@ -16,22 +49,22 @@ function getObjectStatus(diff: ObjectDiff["diff"]): DiffStatus { function formatSingleObjectDiff( data: ObjectData, - status: DiffStatus + status: ObjectDiffStatus ): 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, @@ -62,7 +95,7 @@ function formatSingleObjectDiff( function getPreviousMatch( previousValue: any | undefined, nextSubProperty: any, - options?: Options + options?: ObjectOptions ): any | undefined { if (!previousValue) { return undefined; @@ -76,15 +109,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 +145,10 @@ function getDeletedProperties( function getSubPropertiesDiff( previousValue: Record | undefined, nextValue: Record, - 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 +156,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 +171,7 @@ function getSubPropertiesDiff( ); if (!!!previousMatch) { return subPropertiesDiff.push({ - name: nextSubProperty, + property: nextSubProperty, previousValue: previousMatch, currentValue: nextSubValue, status: @@ -148,7 +183,7 @@ function getSubPropertiesDiff( }); } if (isObject(nextSubValue)) { - const data: Subproperties[] = getSubPropertiesDiff( + const data: SubProperties[] = getSubPropertiesDiff( previousMatch, nextSubValue, options @@ -159,7 +194,7 @@ function getSubPropertiesDiff( } if (previousMatch) { subPropertiesDiff.push({ - name: nextSubProperty, + property: nextSubProperty, previousValue: previousMatch, currentValue: nextSubValue, status: getValueStatus(previousMatch, nextSubValue, options), @@ -173,7 +208,10 @@ function getSubPropertiesDiff( export function getObjectDiff( prevData: ObjectData, nextData: ObjectData, - options?: Options + options: ObjectOptions = { + ignoreArrayOrder: false, + showOnly: { statuses: [], granularity: "basic" }, + } ): ObjectDiff { if (!prevData && !nextData) { return { @@ -204,7 +242,7 @@ export function getObjectDiff( }); } if (isObject(nextValue)) { - const subPropertiesDiff: Subproperties[] = getSubPropertiesDiff( + const subPropertiesDiff: SubProperties[] = getSubPropertiesDiff( previousValue, nextValue, options @@ -236,6 +274,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), diff --git a/src/utils.ts b/src/utils.ts index be4d961..4d8d036 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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)) ); diff --git a/test/list-diff.test.ts b/test/list-diff.test.ts index 6ce2b7d..0743d97 100644 --- a/test/list-diff.test.ts +++ b/test/list-diff.test.ts @@ -393,4 +393,64 @@ 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", + }, + ], + }); + }); }); diff --git a/test/object-diff.test.ts b/test/object-diff.test.ts index b01a348..0cbf629 100644 --- a/test/object-diff.test.ts +++ b/test/object-diff.test.ts @@ -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", () => { 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", () => { 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", () => { 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", () => { ], }); }); - 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", () => { nickname: "super joe", }, }, - { discardArrayOrder: true } + { ignoreArrayOrder: true } ) ).toStrictEqual({ type: "object", @@ -422,31 +422,143 @@ 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", + }, + ], + }, + { + property: "type", + previousValue: "sport", + currentValue: undefined, + status: "deleted", + }, + ], + }); + }); + it("showOnly 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("showOnly added and deleted values in depth", () => { + 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", diff --git a/test/utils.test.ts b/test/utils.test.ts index 7dab72f..4db4c47 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -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", () => { { 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", () => { { name: "joe", age: 88 }, ], { - discardArrayOrder: true, + ignoreArrayOrder: true, } ) ).toBeFalsy(); expect( isEqual([false, 55, "hello"], ["hello", 55, true], { - discardArrayOrder: true, + ignoreArrayOrder: true, }) ).toBeFalsy(); });